Case Study — Multi-Tenant Donation Platform

Screen every donor.
Tell none of them.

A platform where nonprofits run campaigns and always-on giving pages, and donors give by card, wallet, or grant from a donor-advised fund. Take payments for other organizations and you inherit obligations that have nothing to do with payments — sanctions screening, charity verification, records that hold up years later. Those obligations shaped the architecture rather than arriving after it.

At A Glance ● in build
100% of each gift reaches the charity,
less the processor's own fee
Sector
Nonprofit fundraising, multi-tenant
Scope
Giving · DAF grants · donor CRM · receipts
Stack
Node · Postgres · Cloudflare Workers
Screening
In the donation path, and fails closed
Funds
Direct charges — never a platform balance
The Decisions

Six places the
obvious version is wrong.

Nothing here is exotic engineering. Each one is a default that looks reasonable, ships quickly, and is wrong for a system that moves other people's charitable money — and each is cheap to get right at the start and expensive to retrofit.

× Call a screening vendor from inside the donation path
The watchlists live locally and are queried in about half a millisecond — no third party between a donor and their gift
× Tell a blocked donor why they were blocked
A neutral decline that never mentions a watchlist — telling a designated party they matched is itself the violation
× Let the donation through when the screening service is unavailable
Fail closed. A control that switches itself off during an outage was never a control
× Log the blocks
Log the clears too, because "we screen everyone" is only defensible with the clears on record
× Treat a charity missing from the federal list as unverified and stop it
Advisory only — churches are exempt without ever applying and mostly aren't listed at all
× Assume a payment partner's API is safe to retry
Idempotent reservations and explicit unknown-outcome handling on our side of the call

Compliance as
architecture.

01

Compliance sits in the path

Every donor is screened against the sanctions lists at the one function every donation route passes through to resolve a donor — before a row is written, before a payment object exists, card and donor-advised-fund grants alike. A check that lives beside the critical path instead of inside it is a check somebody eventually routes around.

02

Two lists, two postures

Sanctions screening blocks. Charity-status verification does not. They look like the same feature — match a name against a government list — but sanctions violations carry strict liability, while being absent from the charity roll is ordinary and expected. Same shape, opposite defaults, because the legal exposure is not the same.

03

Ship it dark, then flip a boolean

Commercial tiers and a second grant provider are built, tested and merged behind flags, refusing politely until the day they're switched on. The move to cookie-based auth across two independently deployed apps went out behind three flags that all defaulted to off, with a written rollout order, so nothing broke mid-migration.

A control that switches itself off during an outage was never a control.

Which is why screening fails closed: if the lists cannot be queried, the donation is declined rather than waved through. The same instinct runs through the rest of it — production refuses to boot without its encryption key, unknown outcomes from a payment partner are handled as their own case rather than assumed successful, and the decisions that cleared are recorded alongside the ones that blocked.

Inside The Build

Six problems and
what they cost.

Every one of these carries a tradeoff we took deliberately, and in most cases wrote into the repository next to the code — including the ceiling it imposes and the work that would lift it.

Matching names nobody spells the same way

Donor names and list entries are normalized into a sorted set of tokens — diacritics stripped, punctuation dropped, honorifics and corporate suffixes removed — so a list's "SMITH, John" and a donor's "John Smith" compare as equal. The known gap is written down rather than glossed: there is no fuzzy matching, so transliteration variants can still slip past.

Money safety without a safety net

Donor-advised-fund providers are third-party HTTP APIs with no idempotency guarantees of their own, so every provider routes through one shared skeleton: an idempotent pending reservation, explicit handling for outcomes that come back unknown, and receipts issued from our own record. Per-organization credentials are encrypted at rest, and production refuses to start without the key.

Migrations that run once, on their own

Schema changes run inside the container as it boots, guarded by a database advisory lock so two instances starting at the same moment cannot race each other. There is no separate migrate step to forget, and no window where a new image is serving against an old schema.

A release you have to mean

Merging to the main branch deploys staging automatically, gated on the test suite. Production deploys only when someone pushes a version tag — so going live is a deliberate act naming an exact commit, not a side effect of merging a pull request on a Friday.

One instance, on purpose

Rate-limit counters live in one process's memory, so spreading traffic across instances would silently multiply every limit — five login attempts a minute becomes fifteen across three shards. Traffic is deliberately pinned to a single container until shared counters land. The ceiling is documented in the README with the ticket that lifts it.

Tests that watch the routes themselves

Coverage is enforced in continuous integration at 100% of lines and functions across the business-logic surface, with every exclusion carrying a written reason. Route files are verified by introspecting the built router, so an endpoint cannot be added without a test noticing it exists.

How It Holds Up

The discipline is
mechanical, not moral.

Standards that depend on everyone remembering are standards for a good week. These are checked by machines, on every change, and the ones that cannot be are named as such.

Coverage erodes a percent at a time
A hard gate in CI, scoped to business logic, with a written rationale for every exclusion instead of a quietly shrinking denominator
A secret reaches the repository
Secret scanning across the full history on every run, not just the diff
Two green pull requests break the build once both land
Branches are updated before merge, and a failing main branch opens and updates its own tracking issue rather than waiting to be noticed
Documentation drifts away from the code
Docs change in the same commit as the code — endpoints, environment variables and limitations included
A deploy ships code nobody committed
The container image builds from the committed tree, and the README says so plainly next to the command that would otherwise mislead you
— The threshold we had to tune

A naive substring match against the real lists flagged six of twenty ordinary donor names in our own July testing. "Maria Garcia" is contained inside a longer listed name, and a platform that declines her is worse than useless. Allowing exactly one extra token brought it to three of twenty — and those three are genuine possible matches that a person should look at.

There is no threshold that produces zero false positives and zero misses. The work is choosing where to sit, measuring it against the actual lists, and writing down what the choice still lets through.

— What we don't call finished

The platform is mid-flight, and the README carries a known-limitations section that names the open items outright: list imports are still run by hand rather than on a schedule, the cookie and CSRF migration is still behind flags waiting on the frontend release, and the rate-limiting ceiling stands until shared counters land.

A documentation section that only lists what works is a sales page. The value of writing the limits down is that the next person — including the next us — inherits them as decisions instead of discovering them as surprises.

This is platform engineering, built on API development, with the posture that carries a team through a compliance review.

Handling Other People's Money?

The rules don't wait for version two.

Tell us what your product moves — payments, donations, claims, regulated data — and who you'd have to answer to about it. We will tell you which obligations belong in the architecture now and which can honestly wait.