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 with the two-file structure:

hacker-news/
manifest.json ← what the source is (id, name, schedule, config)
index.ts ← your sync(ctx) function

Open manifest.json and make it match:

manifest.json
{
"id": "hacker-news",
"name": "Hacker News Upvotes",
"description": "Index stories you've upvoted on Hacker News.",
"version": "1.0.0",
"category": "reading",
"schedule": "every 6 hours",
"config": {
"username": {
"label": "HN Username",
"type": "text",
"placeholder": "pg"
}
},
"needs_browser": false
}

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).
  • schedule is human-readable. The Mac app parses it and runs your sync on that cadence.
  • needs_browser: false means this source adapter makes plain HTTP requests — no Playwright, no browser session.

See the manifest.json Reference for every field.

Open index.ts and replace the scaffold with:

index.ts
import type { TroveSource } from "@ontrove/sdk";
interface HnStory {
objectID: string;
title: string;
url: string | null;
story_text: string | null;
author: string;
created_at: string;
}
export default {
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,
}));
},
} satisfies TroveSource;

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 manifest.json

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 the Mac app does 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.