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.
Prerequisites
Section titled “Prerequisites”- Trove CLI installed (
npm install -g @ontrove/cliorbun 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)
Step 1 — Scaffold the source adapter
Section titled “Step 1 — Scaffold the source adapter”trove source init hacker-newscd hacker-newsThis 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 itStep 2 — Declare and implement it
Section titled “Step 2 — Declare and implement it”Everything lives in one defineSource call. Open extension.ts and make it match:
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:
configdeclares the one field the user fills in during setup — their HN username. It becomesctx.config.usernamein your code. Config holds preferences only, never credentials (this source needs no auth at all).egressnames every host the source may reach. It is enforced at run time, so a host you leave out is unreachable rather than merely undeclared.schedulecomes from a closed vocabulary — a cadence Trove does not recognise is refused, rather than quietly becomingdaily.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.jsonis rewritten from this call on every build. Nothing to keep in step by hand.
See the Source Manifest Reference for every field.
Step 3 — What you just wrote
Section titled “Step 3 — What you just wrote”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.
Step 4 — Test it locally
Section titled “Step 4 — Test it locally”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:
echo '{"username":"pg"}' > config.jsontrove 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:
trove source test # run sync against fixtures, assert document shape (offline)trove source validate # validate the manifest half of your declarationStep 5 — Run it for real
Section titled “Step 5 — Run it for real”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:
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)Step 6 — See your documents in Claude
Section titled “Step 6 — See your documents in Claude”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.
What’s next
Section titled “What’s next”- SDK Reference — the full
ctxobject, the document shape, audio transcription, and error handling - Source Manifest Reference — every field you can declare, and its effect
- Cursors and Feeds — incremental sync, cursors, and multi-feed sources
- Ship it: package the folder into the source-adapter catalog so others can add it