2026-09-10

Keep the catalog in sync

Open-source projects are a subset of the orgs this catalog serves. OSS.Fund is the sibling directory for OSS funding platforms (Open Collective, GitHub Sponsors, Liberapay, and the rest). They do not offer an HTTP API or webhooks. Their public source is Markdown on GitHub (CC BY 4.0): oss-fund/directory.

This site therefore:

  1. Lists OSS.Fund itself as a meta-directory.

  2. Imports live OSS-friendly / nonprofit-tagged platforms into the Open source category (pnpm catalog:sync-oss-fund).

  3. Publishes a pull API so they — or anyone — can consume our catalog.

  4. Offers outbound catalog.updated webhooks, plus an inbound GitHub receiver if OSS.Fund (or we) attach a repo webhook later.

  5. Re-verifies stale hand-authored listings with Gemini (pnpm catalog:verify / weekly Verify catalog workflow) and opens a review PR.

Pull the catalog

JSON feed (CORS enabled, 5-minute cache):

GET https://nonprofit-resources.org/api/catalog

Each item includes id, slug, name, href, category, tags, portalUrl, openSource, and verification flags. Filter items where openSource is true for the OSS subset.

Attribution

Imported OSS.Fund rows stay attributed in each listing and on /attributions. If you republish those rows, keep the CC BY 4.0 credit to OSS.Fund.

Outbound webhooks

Staff (signed-in admin email or CATALOG_WEBHOOK_ADMIN_TOKEN) can register a destination:

curl -X POST https://nonprofit-resources.org/api/webhooks \
  -H "Authorization: Bearer $CATALOG_WEBHOOK_ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://example.org/hooks/nr","note":"oss.fund mirror"}'

The response includes a secret once. Every later POST is HMAC-SHA256:

  • Header X-Nonprofit-Resources-Event: catalog.updated

  • Header X-Nonprofit-Resources-Signature: sha256=<hex>

  • JSON body with catalog (URL of /api/catalog) and occurredAt

Production deploys call /api/webhooks/dispatch with CATALOG_WEBHOOK_DISPATCH_SECRET. Polling /api/catalog is enough if you do not want a push.

Private/loopback URLs are rejected.

Inbound GitHub webhook (OSS.Fund)

If the maintainers of oss-fund/directory add a webhook:

That ping starts GitHub Actions (repository_dispatch: oss-fund-sync), which regenerates src/lib/oss-fund-seed.generated.ts and commits directly to main when the import changed (org policy blocks the default GITHUB_TOKEN from opening PRs). A daily cron does the same when no webhook is attached.

Until they add that hook, we stay in sync by cloning their repo on a schedule.

Hand listings: Gemini verify

Vendor programs and apps in src/lib/services-seed.ts drift (caps, seats, eligibility). The Verify catalog workflow runs weekly (pnpm catalog:verify):

  1. Selects hand rows with lastVerifiedAt older than CATALOG_VERIFY_MAX_AGE_DAYS (skips partner_oss).

  2. Fetches each directPortalUrl for evidence (Google Search only if the fetch fails).

  3. Asks Gemini (GEMINI_MODEL, default gemini-2.5-flash-lite) for a JSON schema patch with citations.

  4. Opens a PR via CATALOG_VERIFY_GITHUB_TOKEN (fine-grained PAT — see docs/ENV-VARIABLES.md). Never auto-merges.

Secrets: GEMINI_API_KEY (Bitwarden item nonprofit-resources, Google project 54406180065) and CATALOG_VERIFY_GITHUB_TOKEN in GitHub Actions.

Sending listings the other way

OSS.Fund accepts new platforms as a pull request against posts/ in oss-fund/directory. A listing for Nonprofit Resources belongs there as a directory, not a funding platform, and should wait on their editorial review. Prefer that they poll /api/catalog if they want the full feed.