Skip to content

Quickstart — Your First Source Adapter

This guide walks you through building a working source adapter from scratch. By the end, documents from a real upstream will appear in your Trove knowledge base — searchable from Claude on desktop, web, and mobile.

We’ll build a Hacker News Upvotes source: it reads the stories you’ve upvoted on Hacker News and indexes each one as a document. The upstream is a public JSON API (the Algolia HN search API), so there’s nothing to log into — perfect for a first source adapter. The same shape applies to any RSS feed, JSON API, or any page you sync.

  • Trove CLI installed (npm install -g @ontrove/cli or bun add -g @ontrove/cli)
  • Logged in: trove login
  • Node 20+ or Bun 1.1+
  • A Hacker News username (any account that has upvoted a few stories)
Terminal window
trove source init hacker-news
cd hacker-news

This creates a folder holding your source:

hacker-news/
extension.ts ← what the source is, and what it does
manifest.json ← generated from extension.ts; you never edit it

Everything lives in one defineSource call. Open extension.ts and make it match:

extension.ts
import { defineSource } from "@ontrove/extend/source";
interface HnStory {
objectID: string;
title: string;
url: string | null;
story_text: string | null;
author: string;
created_at: string;
}
export default defineSource({
id: "hacker-news",
name: "Hacker News Upvotes",
description: "Index stories you've upvoted on Hacker News.",
icon: "🟠",
version: "1.0.0",
author: "you",
kind: "scheduled-sync",
transport: "api",
cursor: "none",
ingest: "append",
runsIn: "cloud",
schedule: "every 6 hours",
status: "implemented",
needsBrowser: false,
egress: ["hn.algolia.com"],
config: {
username: { label: "HN Username", type: "text", placeholder: "pg" },
},
async sync(ctx) {
const username = ctx.config.username as string;
// The Algolia HN API exposes a public "upvoted_<user>" tag.
const res = await ctx.fetch(
`https://hn.algolia.com/api/v1/search?tags=upvoted_${username}&hitsPerPage=100`
);
if (!res.ok) {
throw new Error(`HN API returned ${res.status}`);
}
const data = (await res.json()) as { hits: HnStory[] };
ctx.log(`fetched ${data.hits.length} upvoted stories for ${username}`);
return data.hits.map((story) => ({
// A stable id is the dedup key — re-running sync never duplicates a story.
id: story.objectID,
title: story.title,
text: `${story.title}\n\n${story.story_text ?? story.url ?? ""}`,
url: `https://news.ycombinator.com/item?id=${story.objectID}`,
author: story.author,
date: story.created_at,
}));
},
});

A few things to note:

  • config declares the one field the user fills in during setup — their HN username. It becomes ctx.config.username in your code. Config holds preferences only, never credentials (this source needs no auth at all).
  • egress names every host the source may reach. It is enforced at run time, so a host you leave out is unreachable rather than merely undeclared.
  • schedule comes from a closed vocabulary — a cadence Trove does not recognise is refused, rather than quietly becoming daily.
  • runsIn: "cloud" is allowed here because the transport is a plain HTTP pull and the source needs no browser. Both are checked when the module is imported.
  • manifest.json is rewritten from this call on every build. Nothing to keep in step by hand.

See the Source Manifest Reference for every field.

The whole contract is: return an array of documents. Each document needs a stable id and some text; title, url, author, and date are optional metadata. See the document shape for every field.

Run the source adapter against your machine without touching the cloud. trove source dev executes sync(ctx) once and prints the documents it would push. The --config flag takes a path to a JSON file of preference values, which becomes ctx.config:

Terminal window
echo '{"username":"pg"}' > config.json
trove source dev --config config.json
loading manifest: hacker-news v1.0.0 (1 config field)
running sync(ctx) …
fetched 37 upvoted stories for pg
✓ sync returned 37 documents
# id title
1 8863 My YC app: Dropbox - Throw away your USB drive
2 9224 Stripe: Atlas
(no documents pushed — dev mode is read-only)

trove source dev runs sync locally and validates the document shape, but pushes nothing. Use it to iterate on your fetch and mapping logic. To run sync against a fixtures file offline — canned responses keyed by request URL — and assert the document shape with no live fetch, use trove source test. To validate just the manifest (shape plus a credential-key lint), use trove source validate:

Terminal window
trove source test # run sync against fixtures, assert document shape (offline)
trove source validate # validate the manifest half of your declaration

When the output looks right, run a real sync. This creates the source and its default feed in your knowledge base (if they don’t exist yet) and pushes the documents via ingestDocuments:

Terminal window
trove source sync --create --config config.json
ensuring source hacker-news … created (src_a1b2c3)
ensuring feed "default" … created (feed_d4e5f6)
reading cursor … (none — first sync)
running sync(ctx) … 37 documents
pushing to ingestDocuments (batch 1/1) …
✓ indexed 37, skipped 0, cursor → (unchanged)

Under the hood this is exactly what your own code could do via the Sync API: ensure a source, ensure a feed, then call ingestDocuments(sourceId, feedId, documents, cursor). Trove stores the full text, embeds it, and indexes it for search.

Run it again and watch dedup work — every story is skipped because its id already exists:

✓ indexed 0, skipped 37, cursor → (unchanged)

The documents are now in your knowledge base. Ask Claude on your Trove connection:

Search my Trove for what I’ve upvoted about startups.

Claude calls trove_search, and your freshly-indexed HN stories come back with [doc:ID] handles. You can also confirm the source exists:

List my Trove sources.

trove_list_sources shows Hacker News Upvotes with its document count.