Case study · Nonprofits & fundraising
A multi-brand fundraising platform, built from the ground up
A multi-tenant, white-label SaaS platform where organizations run campaigns, every participant gets a personal fundraising page, and donations, sales, tickets, and auction bids flow through one Stripe Connect payment pipeline.
- Client
- Peer-to-peer fundraising SaaS
- Services
- SaaS product development · Payments & marketplace integration · APIs & systems integration · Marketing sites & SEO · Testing, quality & DevOps
Highlights
Multi-tenant and white-label
Brands, partners, and organizations, each with the right access and nothing more. Every brand gets its own domains, logo, colors, and email sender from a single build.
Payments that pay out to the right people
Stripe Connect sends each gift to the right connected account - the organization’s, a partner’s, or the brand’s - so the platform never holds their money.
Webhook-driven, concurrency-safe totals
Totals only move when verified Stripe webhooks confirm the money, and stay right when events retry, arrive out of order, or land at the same moment.
One pipeline, many ways to raise money
Donations, product sales, holiday shops, event tickets, raffles, and auctions all share one payment, crediting, and ledger pipeline.
Refunds, voids, and chargebacks
Money moving backwards is split pro-rata across the gift, the fee, and any partner share, with every step recorded and safe to retry.
A configurable fee engine
Fees resolve from campaign, organization, partner, and brand settings, with optional partner revenue sharing and a donor tip that can offset the organization’s fee.
A real financial ledger
Every gift, fee, refund, and chargeback writes an auditable ledger entry - the source for reports, payout statements, and performance dashboards.
An audit trail you can follow
Every request gets a correlation id that ties together its logs, the commands it ran, and the field-level changes it made.
Automated, monitored operations
GitHub Actions builds and deploys to AWS with health checks and automatic rollback, and logs and server metrics flow to CloudWatch.
The challenge
The client wanted to give schools, youth sports teams, nonprofits, and churches a way to run peer-to-peer fundraisers themselves - without a sales call, setup fees, or a vendor taking weeks to configure a campaign. An organization runs a central campaign; each supporter gets their own personal fundraising page; every gift rolls up to their team and the campaign total in real time.
That sounds simple. The hard parts were underneath:
- Money has to go to the right place. Donations belong to each organization, not the platform.
- Totals have to be right. A leaderboard that’s wrong - or double-counts a retried payment - erodes trust immediately.
- Most users are on a phone. Organizers, participants, and donors mostly arrive from a text message or social post.
- It has to grow. Donations were only the first fundraising type; raffles, auctions, and product sales were on the roadmap - and fundraising companies wanted to offer the platform under their own brands.
Our approach
We built the platform as deployable pieces sharing one repository: a .NET 10 Web API, a Vue 3 + TypeScript dashboard, and static Astro marketing sites, with brand design tokens shared between the frontends so they always look like one product.
A backend structured to grow
The API follows Clean Architecture - Domain, Application, Infrastructure, and API layers with dependencies pointing inward. Features are expressed as MediatR commands and queries, with FluentValidation running as a pipeline behavior so every request is validated the same way. Controllers stay thin; business rules live where they can be unit tested.
We made several data-model decisions early, before real data existed, because they would have been painful to retrofit later:
- Separating an organization from the people who administer it, so an organization can have many admins.
- Separating a person’s identity from their participation in a specific campaign, so the same supporter returning next year links back to their existing profile.
- Separating a generic payment record from what it paid for, so new fundraising types reuse the same payment pipeline instead of duplicating it.
That last decision paid off exactly as planned: when product sales, raffles, and auctions arrived, each one plugged into the existing payment, crediting, and ledger code instead of growing its own.
Payments done properly
Online payments use Stripe’s Payment Element and Stripe Connect Express, so each organization onboards once and is paid directly. Payment success is confirmed by signature-verified webhooks, which update participant, team, and campaign totals in a single transaction. Organizers can also record cash and check gifts by hand, and they count toward the same totals.
Because webhooks can arrive several at once while an admin acts on the same donation, every money change follows strict rules: row locks taken in a fixed order, atomic increments instead of read-then-write totals, and status checks only while holding the lock. Integration tests force the race conditions that would break those rules.
Fees resolve through a small, well-tested fee engine - campaign, organization, partner, and brand overrides, then the platform default. An optional donor tip offsets the organization’s fee, and partners can keep a configured revenue share of their organizations’ gifts. Every gift, fee, and share writes an entry to an organization ledger.
When money moves backwards
Refunds, voids, and chargebacks are where many payment systems quietly go wrong. Here, each one is split pro-rata across the gift, the platform fee, and any partner share, with cumulative rounding so several partial refunds add up exactly to a full one. Refunds are recorded before Stripe is called and finished by webhook if anything fails midway. Chargebacks are mirrored from Stripe, lock the donation while open, and restore exactly the same shares if the dispute is won.
More than donations
On the same pipeline, organizations can now run:
- Product sales - a catalog, online checkout from each participant’s page, paper order entry, and fulfillment reports.
- Holiday shops - vouchers parents buy online and students spend at an in-person shop.
- Event tickets - seats and tables, guest questions for the caterer, QR tickets, check-in, and receipts that show the deductible amount.
- Raffles - licensed, numbered tickets online and on paper, with verifiable drawings.
- Auctions - online timed, silent, live, and fund-a-need, with proxy bidding, soft close, buy-it-now, and pay links for winners.
- Volunteer sign-ups - shifts, roles, and items to bring, with a waitlist, reminders, and check-in.
Each new fundraising type ships behind its own switch, so it can be built and tested end to end while questions like sales tax and raffle licensing are settled before it goes live.
Many brands, one platform
The platform is white-label: a brand has its own domains, logo, colors, email sender, and default fee, and fundraising companies manage their own organizations as partners under it. The brand is resolved from the host on every request, the dashboard is themed at runtime from one build, and Stripe routing can send money to each organization, each partner, or the brand. Partners get their own performance snapshot and an organization payouts report.
Email that runs itself
Campaign emails come from a library of templates and schedules maintained by platform staff, which protects the platform’s sending reputation while organizers choose recipients and timing. A background worker delivers them through SendGrid, and a signed event webhook records deliveries, clicks, bounces, and unsubscribes so organizers can see engagement - down to each email’s delivery history.
An audit trail you can follow
Every request gets a correlation id that appears on its log lines, in a log of every command it ran (with secrets stripped), and on a field-level change log written in the same save. Raw Stripe events are kept before they’re handled, so staff can see exactly what happened to a donation and safely replay an event if needed.
Mobile-first everywhere
The dashboard was built for a ~375px screen first: stacked forms, full-width tap targets, card lists in place of wide tables, bottom-sheet dialogs, and form controls sized so phones don’t zoom on focus. Rosters are built for a thousand participants, with fast in-browser filtering.
Marketing sites that rank and load instantly
Each brand’s public site is built with Astro and ships as plain, pre-rendered HTML - with structured data, a sitemap, social share cards, Markdown-driven content pages, cookieless analytics, and small interactive islands only where needed, like a fee calculator.
Quality and operations
The solution has three test projects mirroring the layers they test. Domain and application logic are covered by xUnit unit tests; the API is covered by integration tests that spin up a real PostgreSQL database with Testcontainers for every run, with payment and email providers swapped for fakes so tests stay fast and deterministic.
GitHub Actions builds one release and deploys it to AWS - database migrations, a health check, and automatic rollback if the new version doesn’t come up. A separate demo deployment, reset with realistic sample fundraisers, runs on Stripe test mode so production never mixes with demo data. Logs and disk, memory, and swap metrics go to CloudWatch, with alarms.
The result
A complete, end-to-end platform - from organizer signup and bank onboarding to donation, ticket, or winning bid, ledger entry, and follow-up email - running under multiple brands, and designed so organizers can use it without training. The early architecture decisions held up: each new way to raise money had an obvious place to go.
Architecture at a glance
Dashboard SPA
Vue 3 · TypeScript · Tailwind
Marketing site
Astro · static HTML
.NET 10 Web API
API
ASP.NET Core controllers · JWT auth · versioning
Application
MediatR commands & queries · FluentValidation
Domain
Entities & business rules · no framework deps
Infrastructure
Implements Application interfaces · background workers
PostgreSQL
EF Core · migrations
Stripe Connect
Payments · webhooks
SendGrid
Email · event webhooks