{
"path": "/projects/supporters",
"site": "at://did:plc:ofrbh253gwicbkc5nktqepol/site.standard.publication/3mfyq5mpohw25",
"tags": [
"atproto",
"creative",
"pkgs",
"tooling",
"website"
],
"$type": "site.standard.document",
"title": "@ewanc26/supporters",
"description": "SvelteKit component library for displaying Ko-fi supporters and GitHub Sponsors, backed by an ATProto PDS.",
"publishedAt": "2026-03-09T00:00:00.000Z",
"textContent": "@ewanc26/supporters is a SvelteKit component library for displaying Ko-fi supporters and GitHub Sponsors. Webhook events from both platforms are stored as records on your ATProto PDS and aggregated into presentable supporter lists.\n\nPart of the @ewanc26/pkgs monorepo.\n\nHow it works\n\nKo-fi\n\n1. Ko-fi POSTs a webhook event to /webhook on each transaction\n2. The handler verifies the verification_token, respects is_public, and calls appendEvent\n3. appendEvent writes a record to your PDS under uk.ewancroft.support.kofi\n4. readStore fetches all records and aggregates them into KofiSupporter objects\n5. Pass the result to <KofiSupporters> or <LunarContributors>\n\nGitHub Sponsors\n\n1. GitHub POSTs a sponsorship webhook event to /webhook/github on each sponsorship change\n2. The handler verifies the HMAC-SHA256 signature, respects privacy_level, and skips pending_* actions\n3. appendSponsorEvent writes a record to your PDS under uk.ewancroft.support.github\n4. readSponsors fetches all records and replays them chronologically to derive the current state (active/inactive, current tier) per sponsor\n5. Pass the result to <GitHubSponsors>\n\nInstall\n\nRequires svelte >= 5 and @atproto/api >= 0.13.0 as peer dependencies.\n\nSetup\n\nEnvironment variables\n\nGenerate an app password under Settings → App Passwords on witchsky.\n\nRegister the Ko-fi webhook\n\nSet your webhook URL to https://your-domain.com/webhook in ko-fi.com/manage/webhooks.\n\nRegister the GitHub Sponsors webhook\n\nIn your GitHub Sponsors settings, add a webhook pointing to https://your-domain.com/webhook/github. Set the content type to application/json, choose a secret, and subscribe to Sponsorship events only.\n\nAdd the routes\n\nCopy src/routes/webhook/+server.ts and src/routes/webhook/github/+server.ts from the package into your SvelteKit app's routes directory.\n\nUse the components\n\nComponents\n\n<KofiSupporters>\n\nDisplays all Ko-fi supporters with emoji type badges (☕ donation, ⭐ subscription, 🎨 commission, 🛍️ shop order).\n\n| Prop | Type | Default |\n|---|---|---|\n| supporters | KofiSupporter[] | [] |\n| heading | string | 'Supporters' |\n| description | string | 'People who support my work on Ko-fi.' |\n| filter | KofiEventType[] | undefined (show all) |\n| loading | boolean | false |\n| error | string \\| null | null |\n\n<LunarContributors>\n\nConvenience wrapper around <KofiSupporters> pre-filtered to Subscription events only.\n\n<GitHubSponsors>\n\nDisplays GitHub Sponsors with their tier name. Each card links to the sponsor's GitHub profile.\n\n| Prop | Type | Default |\n|---|---|---|\n| sponsors | GitHubSponsor[] | [] |\n| heading | string | 'GitHub Sponsors' |\n| description | string | 'People who sponsor my work on GitHub.' |\n| activeOnly | boolean | true |\n| loading | boolean | false |\n| error | string \\| null | null |\n\nServer utilities\n\nKo-fi\n\nreadStore(): Promise<KofiSupporter[]>\n\nFetches all uk.ewancroft.support.kofi records from the PDS (no auth required) and aggregates them by name into KofiSupporter objects. Reads are paginated automatically.\n\nappendEvent(name, type, tier, timestamp, opts?): Promise<void>\n\nWrites a single Ko-fi event as a new record. Uses ATPROTO_APP_PASSWORD for authentication. The rkey is a TID derived from the transaction timestamp via @ewanc26/tid.\n\nparseWebhook(request, opts?): Promise<KofiWebhookPayload>\n\nValidates and parses an incoming Ko-fi application/x-www-form-urlencoded webhook request. Throws WebhookError on invalid token, wrong content-type, or malformed JSON.\n\nGitHub Sponsors\n\nfetchSponsorEvents(did): Promise<GitHubSponsorEvent[]>\n\nFetches all uk.ewancroft.support.github records from the PDS (no auth required) and returns them as a flat chronological timeline — one entry per event, sorted most-recent-first. Used by the website's unified supporters feed.\n\nreadSponsors(): Promise<GitHubSponsor[]>\n\nFetches all uk.ewancroft.support.github records from the PDS (no auth required) and replays them chronologically to produce the current state per sponsor. A sponsor is considered active if their most recent event is created or tier_changed, and inactive after cancelled.\n\nappendSponsorEvent(login, name, action, tierName, monthlyUsd, timestamp): Promise<void>\n\nWrites a single GitHub sponsorship event as a new PDS record. Uses ATPROTO_APP_PASSWORD for authentication.\n\nparseGitHubSponsorsWebhook(request, opts?): Promise<GitHubSponsorshipWebhookPayload>\n\nValidates an incoming GitHub sponsorship webhook request by verifying its HMAC-SHA256 signature against GITHUB_WEBHOOK_SECRET using the Web Crypto API. Throws GitHubWebhookError on signature mismatch, wrong event type, or malformed JSON.\n\nTypes\n\nImporting historical Ko-fi data\n\nExport your transaction history from ko-fi.com/manage/transactions → Export CSV, then run the bundled import script:\n\nRemove --dry-run to write records. The script is idempotent — re-running merges new event types and tiers into existing records.\n\nLexicons\n\nuk.ewancroft.support.kofi\n\nuk.ewancroft.support.github\n\nrkeys are TIDs derived from the event timestamp, making all records lexicographically sortable by time.\n\nLicence\n\nAGPL-3.0-only — see the pkgs monorepo.",
"canonicalUrl": "https://docs.ewancroft.uk/projects/supporters"
}