CharityRight — Donation Infrastructure Rebuild
I joined a UK charity where the technology was in fragments. The website had been built by an external agency. Every donate button on the site still routed through their hosted checkout — a system the charity didn't own or control. The mobile donation experience took over 30 seconds to navigate, with double form fills and a clunky UI. Donations were scattered across three separate platforms — the agency's checkout, Enthuse, and LaunchGood — each with different payout cycles (2 days vs weekly vs sometimes two months), different fee structures, and inconsistent Gift Aid handling. The CRM integration was broken: donation data wasn't flowing into the system properly. Gift Aid reconciliation was largely manual.
Over 18 months as the sole developer, I systematically replaced every external dependency — 14 repositories in total. Built a new checkout from scratch — multi-step with progressive disclosure, Stripe PaymentIntents for one-off, SetupIntents with off-session charging for recurring (the system only allows amount increases on active plans — a deliberate constraint). Apple Pay and Google Pay. Three separate donation builder components with Zod validation. Built the P2P fundraising engine — individual fundraiser pages, team pages, leaderboards, URL-based attribution, all backed by a Prisma schema with 15 interconnected models. Built a config-driven campaign landing page system with a 30-field campaign interface — slug, amounts, impact messaging, widget type, social proof, custom HTML support — so the fundraising team could launch Ramadan appeals without waiting for a developer.
Then I built the data layer. Three separate sync pipelines for each donation platform: Enthuse required Playwright browser automation with a four-layer auth chain — SSO login triggers a magic-link email, Gmail OAuth reads it, Playwright clicks it, then optional TOTP via speakeasy. LaunchGood required Playwright to navigate the export UI, check an acknowledgment checkbox, and capture the CSV download stream. Both use session persistence via storageState.json, safeClick() retries, and screenshot dumps on failure. A third pipeline syncs the charity's own MySQL database into PostgreSQL — five data streams (one-time donations, scheduled recurring, supporters, appeals, legacy donations), each joining 10+ source tables, landing in raw tables then transforming into a canonical model. That canonical model feeds N3O CRM queue payloads and a donor messaging system built on n8n + Chatwoot + WAHA with a database-backed send queue. Terminated the agency retainer. Migrated hosting.
The decision that mattered
Nobody told me to handle Zakat compliance. I just knew that if a Muslim donor selects Zakat, the system needs to give them a clear choice about admin fees — because Zakat is a religious obligation with specific rules about how the money is used, and that choice needs to be explicit, not buried. I built it as a per-donation and per-plan toggle so the donor controls what happens, not the system. Nobody told me to move Gift Aid capture to post-payment either. But asking a donor for their home address before they've committed to giving kills conversion. I moved it. HMRC still gets what they need. The charity gets more donations. These aren't in any spec. They're the kind of decisions you only make if you understand the domain, not just the technology.