Sources in, feeds out. An open platform for databases that only ever grow: game releases, package registries, government filings, and any other public data with a big seed and daily growth. One Postgres, one Redis, and every row reachable by web, RSS, JSON, API, CLI and MCP.
The first deployment is nichedb.dev. Run your own on anything that has Postgres and Redis.
| Thing | What it is |
|---|---|
| collection | A niche: games, packages, filings. |
| source | One adapter pointed at one upstream, fetched on its own cadence. Steam's new-releases list. The npm changes feed. EDGAR Form D filings. |
| item | One row a source produced. Title, URL, when (with time_known and precision), tags, and the adapter's payload in data. |
| feed | A saved query over a collection. Has a page, RSS and JSON Feed renderings, an API endpoint, and followers who are told when it changes by push, email or signed webhook. |
Adapters are one file each in packages/adapters/src. One hundred and twenty-eight ship today across forty-one collections:
| Collection | Adapters | Key needed |
|---|---|---|
| games | steam, steam-news, igdb, igdb-catalog (every game IGDB knows, walked by id then kept current from updated_at), wikidata-games, steam-catalog |
IGDB only (Twitch client) |
| packages | npm, pypi, crates, go-modules, huggingface, github-releases |
no (GitHub token optional) |
| filings | edgar, federal-register, courtlistener |
CourtListener only |
| music | musicbrainz, musicbrainz-catalog, discogs-catalog |
no |
| books | openlibrary, gutenberg-catalog, librivox-catalog, openlibrary-catalog |
no |
| tabletop | scryfall-sets, scryfall-cards |
no |
| space | launch-library |
no |
| chess | lichess-broadcasts |
no |
| alerts | usgs-earthquakes, gdacs |
no |
| weather | nws-alerts, nhc-cyclones, swpc-space-weather, eonet-events |
no |
| outages | statuspage (any Statuspage host) |
no |
| extensions | firefox-addons, vscode-extensions, mcp-registry |
no |
| health | openfda-recalls, clinical-trials |
no |
| research | arxiv, crossref |
no |
| automotive | fueleconomy-catalog, nhtsa-recalls, nhtsa-complaints, nhtsa-safety-ratings |
no |
| markets | iso-mic-exchanges, alpaca-corporate-actions, alpaca-news, alpaca-assets, equity-history, nasdaq-halts, ecb-fx-rates, sec-fundamentals |
Alpaca (APCA_API_KEY_ID, APCA_API_SECRET_KEY, APCA_FEED iex or sip); SEC wants CONTACT_EMAIL in the user agent |
| crypto | coingecko-assets, crypto-pairs |
keyless (COINGECKO_API_KEY optional; CRYPTO_PROXY_URL for Binance.US from a datacenter) |
| crime | socrata-crime, uk-police-crime, fbi-crime-estimates, police-updates (39 California police feeds; source coverage) |
FBI only (free api.data.gov key) |
| public-money | usaspending-awards, ocds-tenders, ted-notices |
no |
| housing | uk-land-registry, freddie-mac-rates, building-permits, ruuster (agent saved searches) |
no |
| jobs | bls-series, eurostat, warn-layoffs, agenticjobs |
no |
| ai-incidents | rogue-ai-incidents, rogue-ai-research, aiid-reports |
no |
| news | newsfeed, gdelt, rssamplifier, brisk, news-channels |
no |
| domains | ntld-totals, ntld-tlds, ntld-launches, ntld-changes |
no |
| podcasts | podcasts, p0dcasters, podcastindex-catalog |
no |
| aviation | faa-nas-status, aviation-hazards, aviation-metar, ntsb-accidents, adsb-flights |
no (NTSB needs mdbtools + unzip, in the Dockerfile) |
| water | nwps-river-gauges, coops-water-levels, drought-monitor, ndbc-buoys, nws-surf-zone |
no |
| consumer-finance | cfpb-complaints, fdic-institutions, fdic-structure-changes |
no |
| deals | slickdeals, dealnews, dealcatcher, bensbargains, reddit-deals |
no |
| sports | espn-catalogue, espn-schedule, espn-live, espn-plays, livetennis, sportsdb-tv, sportsdb-leagues, sportsdb-teams |
Live Tennis; TheSportsDB (SPORTSDB_API_KEY, the shared key 3 returns one row per query); ESPN is keyless (SPORTS_PROXY_URL for cloud egress) |
| screen | tmdb-releases, tvmaze-schedule, tvmaze-catalog (every show TVmaze knows, CC BY-SA, walked once then kept current from /updates/shows), anilist-airing, imdb-ratings, wikidata-films, kitsu-anime, thetvdb-catalog (every series on TheTVDB, THETVDB_API_KEY, attribution link required) |
TMDB only |
| channels | iptv-org-channels |
no |
| saas | saasrow, opensaas (nichedb.dev first; any service serving /.well-known/opensaas.json) |
no |
| marketplace | d0rz, bl0ggers |
no |
| ai-media | aiornot |
no |
| forums | tsbb |
no |
| coupons | c0upons (what c0upons.com took in on its own: submitted codes and its r/couponcodes listings; rows it copied from deals are not read back) |
no |
| dht | bittorrented-dht (adult material left out) |
bittorrented.com's Supabase URL and publishable key (BITTORRENTED_SUPABASE_URL, BITTORRENTED_SUPABASE_KEY); paused without them |
| hosting | findhost, buyvps, vultr-plans, linode-types, scaleway-instances, ovh-vps, storefront (WHMCS, Blesta and WooCommerce order forms), lowendbox, openserver; hetzner-plans, digitalocean-sizes, upcloud-plans |
no for the first eight (FindHost data is CC BY 4.0: credit FindHost, findhost.app; OBSCURA_MCP_URL optional for JavaScript-only shops); Hetzner (HETZNER_API_TOKEN), DigitalOcean (DIGITALOCEAN_TOKEN) and UpCloud (UPCLOUD_USERNAME, UPCLOUD_PASSWORD) each need a read-only credential and stay paused without one |
| threats | openthreat (ThreatCrush first; any reporter serving /.well-known/openthreat.json) |
no |
| profiles | openprofiles (p0dcasters and OutreachGraph listings first; any app that lists the OpenProfile.md files it serves), sportarr-persons (Sportarr's 111k names, kept only when Wikidata knows the person as a human with a sport, with their socials), sportsdb-players |
no |
nichedb serves its own OpenSaaS descriptor at /.well-known/opensaas.json: the plans as the checkout sells them (prepaid terms, none renews itself) and the actions that are real here, each as the page a person opens and the endpoint an agent calls with an OpenAccess bearer: subscribe (POST /api/v1/billing/subscribe, answers pending with the checkout page), cancel (POST /api/v1/billing/cancel, one click, today, nothing refunded), unsubscribe (a page: /settings, where the follows that send mail are turned off), export (POST /api/v1/account/export, one JSON document at once), delete (POST /api/v1/account/delete, confirmed by a link to the account email, immediate, nothing kept). The same doors as pages: /account/billing, /account/export, /account/delete. The opensaas adapter reads other services' descriptors into the saas collection, one row per plan with the service's own steps and confirm for the way in and the way out shown beside each other.
One entry per person, assembled from every app that serves their OpenProfile.md. The openprofiles adapter pages each app's listing (/api/openprofiles?since=…), fetches every document from the host that listed it, and merges documents that share an identity key (the Web URL, any Accounts URL, Email, DID) into one profile; a shared name alone never merges. The page is /c/profiles/<slug>-<id> (the id resolves, the name is cosmetic, a wrong name 301s) or /c/profiles/<handle> once the owner took one, and …/openprofile.md next to it is the file, text/markdown, CORS open, with <link rel="openprofile"> on the page.
A person claims their entry with This is me (proven by the email the profile lists, or a link back to the page from their site or show; admins may claim for an email) and edits it from the page, PUT /api/v1/profiles/<ref> (JSON overlay or a whole text/markdown file), nichedb profile edit, or the update_profile MCP tool, with a session, an API key, or an OpenAccess token carrying openprofile:edit. What the owner writes is an overlay that wins over every source and survives every re-read; a section written as none is removed. The Broadcast and Guest sections (OpenBroadcast, OpenGuest) are what the podcasters and guests feeds cut on.
| directory | outreachgraph | no |
| sites | opensite | no |
A niche can have moderators: people who review what is suggested for its collection without operating it. Anyone signed in applies at /<niche>/moderate (three questions); the application is a claim marked with the role, listed on /admin/knowledge with every other claim, and only an admin decides it. An approved moderator is a niche member with a share cap of zero and no score row: moderating earns nothing and is not a rung on the ladder. More than one person may moderate a niche. Moderators see the suggestion queue at /admin/submissions filtered to the collections their niches feed, approve into that collection only, and get the Queue link in the nav.
One record per page, read the way OpenSite says: the title, the line, the picture and the kind a card is drawn from, the canonical address it is keyed by, the author, the feeds, and the og:, twitter: and JSON-LD tags verbatim. The opensite adapter walks each house site from its front page and sitemap, a hundred pages a run, honouring robots.txt and a site's own /.well-known/opensite.json. Any other address is read the moment somebody pastes it at /c/sites/add, which then shows every tag and the card as X, Slack, iMessage, Discord, LinkedIn and WhatsApp would draw it; the same read is POST /api/v1/sites { "url" }, and GET /api/v1/sites?url= answers the kept record or reads the page when it is older than an hour. A whole list, up to ten thousand addresses pasted or uploaded on the same page (or POST /api/v1/sites/bulk with JSON urls or a text body), becomes a source of the submitter's: the worker walks it 250 pages a run, a minute apart, and /s/<slug> shows the progress; a signed-in account is needed, since a list makes the deployment fetch on a schedule. The page is /c/sites/<host>/<path>, query string and all, because a page's identity can be in its query.
Ten of the sites we run publish a public feed or API of their own, and each is read here through it, keyless, the way any other reader would. The endpoint is the one checked live on 2026-09-12 (c0upons on 2026-09-13); the slug is the seeded source.
| Site | Endpoint | Collection | Source slug | Kinds |
|---|---|---|---|---|
| p0dcasters.com | /opml (every show: feed, site, title) |
podcasts | p0dcasters-shows |
show |
| saasrow.com | /api/v1/products (offset paged, 100 a page) |
saas | saasrow-products |
product |
| d0rz.com | /asks/rss.xml, /offers/rss.xml |
marketplace | d0rz-marketplace |
ask, offer |
| bl0ggers.com | /asks/rss.xml, /offers/rss.xml |
marketplace | bl0ggers-marketplace |
ask, offer |
| aiornot.vote | /rss.xml, /rss/featured.xml, /rss/trending.xml |
ai-media | aiornot-media |
submission |
| agenticjobs.work | /api/v1/jobs (offset paged, 100 a page) |
jobs | agenticjobs-postings |
job |
| outreachgraph.com | /api/v1/public/directory (cursor paged, 200 a page; companies and sites by domain, people only when self-published) |
directory | outreachgraph-directory |
company, site, person |
| tsbb.dev | /api/v1/forums, then /f/{slug}/feed.xml per forum |
forums | tsbb-topics |
post |
| c0upons.com | /api/coupons (offset paged, 200 a page, store joined in; rows with source: nichedb skipped) |
coupons | c0upons-coupons |
coupon |
| bittorrented.com | PostgREST rpc/browse_dht_torrents on the site's own Supabase, by date from a cursor, 500 a page (the site has no submit or export endpoint) |
dht | bittorrented-dht |
torrent |
| c0ncerts.com | none yet: /api/events answers 501 "coming soon" and /api/v1/events 404s |
— | — | — |
saasrow's /api/v1/listings is per-account and needs a key, so only the public products directory is read. A submission aiornot carries on more than one feed is stored once and tagged with each feed. tsbb's cross-board /api/v1/latest does not say which forum a topic is in, which is why the walk is per forum.
ruuster-san-jose-homes reads the Real Estate Experts / Talar Davoudi saved
search hourly and feeds /f/san-jose-homes. The search asks for San Jose houses
that are Active or Coming Soon, with 2+ bedrooms, 1+ bathrooms, 750+ square feet,
a 4,500+ square foot lot and a build year of 2000 or later. Ruuster calls the
lot filter lotSizeAcresMin but accepts square feet there; property records
return acres, and NicheDB keeps both units. Results and dimensions are
upstream observations; conflicting MLS values are not silently corrected.
Create another ruuster source with its savedSearchUrl, pages (20 by
default, ten records per page) and currency (USD or CAD). Tracking parameters
are removed and repeated status filters are combined. Public detail requests
add photos, property facts and MLS attribution. Syndicated copies are joined
by MLS listing number and address. Hidden-address and deleted records are
omitted. The worker's detail budget and deadline are respected, with a cursor
for unfinished pages; completed walks start from page one on the next refresh.
Items retain the last status observed while matching the search. A property disappearing from a search is not evidence that it sold, and this source does not revisit every historical listing after it leaves the search.
Run bun run ingest ruuster-san-jose-homes against the configured database, or
export without a database with bun scripts/scrape-ruuster.js > properties.json.
The export also accepts a saved-search URL and an optional new output filename
as positional arguments. Ruuster's terms restrict automated access without
written consent, and its IDX notice limits reuse; deploying this adapter does
not grant redistribution rights: https://realestateexperts.ruuster.com/terms-of-service.
MusicBrainz web-service requests share a process-wide queue with at least 1.1
seconds between requests. A 429 or 503 retries up to three times with 5, 15 and
45 second backoff, honoring longer Retry-After values, including HTTP dates.
A zero retry hint cannot cause an immediate retry. This queue covers one
worker process; deployments sharing an egress IP across multiple processes
need coordination across those processes as well.
After ingest, every item is enriched by the enrichers that apply to it (packages/enrichers/src), and the results live on the item under enrichment.<name>:
| Enricher | Adds | Default on for |
|---|---|---|
youtube |
top videos (trailers, official audio, webcasts); Data API key optional | games, music, tabletop, space, chess, books |
wikipedia |
the article's lead paragraph and picture | games, music, books, space, tabletop, hosting |
github-repo |
stars, forks, topics, licence, language, last push | packages, extensions |
npm-stats |
last week's downloads | packages |
sec-company |
tickers, exchange, industry, state, website of the filer | filings |
company-ticker |
the listed company behind a hosting provider: ticker and CIK from the SEC's company list, matched on name | hosting |
developer |
the official CLI and its install commands (read off the vendor's guide, or matched by domain in Homebrew, npm and the Terraform registry), API docs, Terraform provider, status page | hosting |
semantic-scholar |
TL;DR, citation counts, open-access PDF (key optional) | research |
openlibrary-work |
description and subjects | books |
opengraph |
the page's own preview image and description | most collections |
tmdb-artwork |
poster, backdrop, synopsis, rating and TMDB id for a title that came in from the IMDb dumps with none (TMDB_API_KEY) |
screen |
A feed's enrichers list picks which of these it shows; absent means the collection's defaults. The feed builder exposes them as checkboxes. Items are enriched once, newest first with a fair share per collection (ENRICH_PER_RUN a tick, ENRICH_PER_COLLECTION of them from any one collection), and a missing image, summary or tags are filled from whatever the enrichers found while the source's own words always win.
git clone https://github.com/profullstack/niche-db && cd niche-db
bun install
docker compose up -d db redis # or point at your own Postgres 16+ and Redis
cp .env.example .env # DATABASE_URL, SITE_URL, CONTACT_EMAIL at minimum
bun run build:client
bun run dev # web + worker in one process on :3000Migrations apply themselves on boot. Every collection, its default sources and its feeds are seeded on first boot; sources whose adapter needs a credential the deployment lacks are created paused. The first account to sign in is an admin.
bun run ingest [slug ...] runs sources from a terminal without Redis. bun test runs the suite against an in-process Postgres (PGlite), so it needs no server.
- Web: a PWA.
/sourcesmanages sources (status, last run, next run, run now, pause, edit config)./feeds/newbuilds a feed with a live preview./settingsholds passkeys, API keys and notifications. - RSS / JSON Feed:
/f/<slug>.rss,/f/<slug>.json. - API:
/api/v1, documented at/docs/api. Reads need no key. - CLI:
npm i -g @profullstack/nichedb→nichedb. Documented at/docs/cli. - MCP:
/mcp(Streamable HTTP, stateless) ornichedb mcpover stdio. Documented at/docs/mcp. - llms.txt:
/llms.txtdescribes the whole deployment in one document.
Magic link and passkey only; there is no password column. API keys are minted from settings and shown once.
Free pages and feeds carry one CrawlProof ad and a tracker (CRAWLPROOF_AD_SLOT, CRAWLPROOF_SITE_ID). Two tiers buy them away, both through CoinPay:
- Premium — $1 a day, $30 a month or $300 a year (
/premium). No ads, no tracking, the members' Lounge, 1,000 credits a month to give awards with, a badge, six themes and five app icons, early access to new collections, 30,000 API requests an hour, unlimited feeds, own sources, and the metered vehicle lookups included./premiumcarries a line-by-line comparison with Reddit Premium;GET /api/v1/premiumis the same thing as JSON. See docs/premium.md. - Pro — $120 a month (
/pro). Everything Premium has, plus 120,000 API requests an hour and a crawl pass for the whole term (GET /api/v1/crawl-pass) so a member's own agents walk through the paywall.
Referral links give the new customer 20% off and the referrer 60% of the first payment.
Training crawlers (GPTBot, ClaudeBot, CCBot, meta-externalagent…) get a 402 with an x402 offer and buy a pass at /crawl: $1 a day for everything, and the more a buyer has paid here the less a day costs (CRAWL_LOYALTY, default 20% off after $10, 40% after $50, 60% after $100; every sale is a row in crawl_sales). A pass may switch ads or tracking off for its own requests with ?disable=ads,tracking. People, search engines and retrieval crawlers pass through untouched.
Every collection here is also a niche someone can operate. A Knowledge Influencer is a person who knows how an industry actually works and supervises the agents building software, data and promotion for it — no code required. They start at 20% of what the niche makes and climb to 80% as verified contribution accumulates.
/opportunities lists the niches looking for one. /<niche> is the niche's
public page (plus skill.md and manifest.json for agents), /@<handle> is an
operator's profile, /dashboard/niches is their own view, and
/admin/knowledge is where claims and contributions are verified.
When an agent gets stuck on something only a person who has done the job can settle, it asks: the question lands on the operator's dashboard, the answer becomes niche knowledge and a scored contribution, and Chovy is told. Saying "not enough context" is a first-class answer that costs nothing, because a guess that gets verified is worse for the niche than an open question. See docs/agent-questions.md.
Score comes only from contributions somebody verified, and volume does not buy
it: repeated submissions of a type pay less each time, anything claiming a
customer or a payment needs an outside reference, and a duplicate books once.
The ladder lives in contribution_tiers, so a deployment can tune it. What a
niche earns is divided at the moment it settles, using the shares in force
right then, so a tier that moves tomorrow never re-prices yesterday's sale
(docs/revenue-ledger.md). See
docs/knowledge-influencers.md and
docs/revenue-share.md.
Bun, Hono (server-rendered JSX), Postgres via Bun's native driver, BullMQ on Redis, Biome. bun apps/worker/src/enrich-cli.js [n] runs enrichment from a terminal. Shared Profullstack modules: @profullstack/x402-gateway, @profullstack/emailer, @profullstack/coinpay, @profullstack/referrals, @profullstack/api-key-manager, @profullstack/autoblog (signed webhooks), @profullstack/favicon-generator.
brand/logo.svg is the editable SVG master. Run bun run build:brand to crush it
into favicon.svg with SVGCrusher, then run the installed fav CLI to regenerate
apps/web/public/icons. The build also updates /logo.svg, /favicon.svg, member
palettes, and maskable app icons. SVG is preferred in the page and app manifest;
PNG and ICO remain fallbacks. Preview everything in brand/preview.html.
SVGCrusher's initial CLI release is bundled in vendor/ so this command works
from a clean checkout before an npm release. Its standalone source lives in
profullstack/svgcrusher.
Everything is read once from the environment in packages/config/src/index.js; .env.example lists every variable. Production secrets live in the nichedb--prod logicsrc vault:
bun run secrets:pull # logicsrc teams pull profullstack nichedb prod --env .env
bun run secrets:push # after adding a key locallyROLES=web,worker runs everything in one container; ROLES=worker on a second service splits ingestion out when one instance stops being enough.
One Dockerfile, one Railway service, plus Railway's Postgres and Redis. railway.json sets the healthcheck at /healthz. Set SITE_URL to the public origin before anyone registers a passkey: the passkey relying-party id is derived from it.
import { defineAdapter } from '@nichedb/core/adapter';
export const example = defineAdapter({
name: 'example',
title: 'Example API',
collection: 'packages',
description: 'What it watches, in one paragraph.',
kinds: ['thing'],
cadenceMinutes: 30,
configFields: [{ key: 'topic', label: 'Topic', required: true }],
defaultSources: [{ slug: 'example-default', name: 'Example', config: { topic: 'all' } }],
async pull({ config, cursor, http, log, budget, deadline }) {
const page = await http.json(`https://example.com/api?topic=${config.topic}&since=${cursor.since ?? ''}`);
return {
items: page.results.map((r) => ({ externalId: r.id, kind: 'thing', title: r.name, url: r.url, publishedAt: r.at, tags: r.tags, data: r })),
cursor: { since: page.next },
note: `${page.results.length} things`,
};
},
});Register it in packages/adapters/src/index.js. The core normalises items, hashes them so unchanged rows cost no write, writes in batches, records the run and reschedules.
A source that reads a multi-gigabyte file (MusicBrainz, Discogs, Open Library, Podcast Index) cannot return one array: the file does not fit in memory and the walk does not fit in one run. Instead pull yields batches and the core drains them one at a time, writing each and saving its cursor before reading the next. pull is an async * generator (or returns { items: <async iterable> }, which is the same thing), each yielded value is { items, cursor }, and the generator's return value is the run's { cursor, note, nextInMinutes }.
import { dumpDir, gzipLines } from '@nichedb/core/dump';
export const example = defineAdapter({
name: 'example-dump',
// ...
cadenceMinutes: 60,
budgetMs: 60 * 60_000,
async *pull({ cursor, http, log, deadline }) {
const version = await http.text('https://example.com/dumps/LATEST');
const skip = cursor.version === version ? Number(cursor.skip) || 0 : 0;
if (cursor.version === version && cursor.done) return { cursor, note: 'unchanged' };
const dir = await dumpDir('example');
const file = `${dir}/${version}.ndjson.gz`;
const dl = await http.download('https://example.com/dumps/latest.ndjson.gz', file);
if (!dl.complete) return { cursor: { version, skip }, note: 'download in progress', nextInMinutes: 1 };
let n = skip;
let batch = [];
for await (const line of gzipLines(file, { skip })) {
n += 1;
batch.push(toItem(JSON.parse(line)));
if (batch.length === 500) {
yield { items: batch, cursor: { version, skip: n } };
batch = [];
if (Date.now() > deadline) return { cursor: { version, skip: n }, nextInMinutes: 1 };
}
}
if (batch.length) yield { items: batch, cursor: { version, skip: n } };
return { cursor: { version, skip: n, done: true }, note: 'complete' };
},
});The rules:
budgetMsis the run's wall-clock budget and replacesINGEST_RUN_DEADLINE_MS(4 minutes) for this adapter. The reaper window and the BullMQ lock widen to the largest budget declared, so an hour-long budget is safe to declare. The core hands it back asdeadline; check it between batches and return. A batch written past the deadline ends the run from the core's side too, rescheduling in a minute, but the adapter checking first is what keeps a run inside its lock.- Each batch's
cursoris the position after that batch's items (a line count, a byte offset, a last id) plus whatever identifies the file (a dump date, a version, aLATESTvalue), so a new upstream file resets the walk. It is saved as soon as the batch is in the table. At-least-once delivery is fine: a batch re-written after a crash is an idempotent upsert on(source, externalId)and unchanged rows cost nothing. returnthe final{ cursor, note, nextInMinutes }.nextInMinutes: 1keeps an unfinished walk moving between cadences;done: truein the cursor lets the next run short-circuit when the upstream file has not changed.http.download(url, path, { timeoutMs, headers, onProgress })streams to disk and resumes withRangefrom whatever is already there (append on 206, restart on 200, 416 means whole). It returns{ path, bytes, complete }; oncomplete: false, return and call it again next run. The user agent is sent; Podcast Index refuses requests without one. Discogs ignoresRangeand rate-limits hard: expect a restart and keep requests to a handful an hour.dumpDir(name)is where the file goes: underINGEST_DATA_DIRwhen set (mount a volume there), else the OS temp dir, which a redeploy wipes. The cursor is the walk; the directory is a cache.- Readers:
gzipLines(path, { skip }),xzLines(path, { member, skip })(MusicBrainz:member: 'mbdump/artist', needsxzon the host),tsvJsonLines(path, { skip })for Open Library's five-column rows (each record carries itslineNo),lineOffsetReader(path, { offset })for a plain file you want to seek in,untar(path, dir)andsqliteRows(dbPath, sql)for Podcast Index. Every reader yields every line, soskip + lines readis the file position. Skipping into a compressed file re-inflates from the top (seconds per gigabyte for gzip, slower for xz); a plain-fileoffsetis a seek.
MIT
Use ?lat=41.88&long=-87.62&radius=10 across located collections, searches and feeds. Geographic queries documents the API, CLI/MCP flags, permissioned scanner catalogs, coverage semantics and migration.