A Decade of Donor Records, Rebuilt Without Losing a Row
A school district foundation was running its entire donor and donation operation — years of gift history, school-by-school allocations, the paperwork that turns into tax letters — on a web application that had not been touched since before the current generation of its own programming language existed. It sat on a shared host with no automated tests and no deployment process. Its login system hashed passwords with a scheme the foundation's own security audit flagged as one that should never be carried forward.
None of that data was expendable. Years of donor history, tied to real people and real gifts, doesn't come back if it's lost in a migration.
The foundation needed a modern system before the old one turned from a known risk into an actual outage — and it needed to know, before it switched over, that nothing had been left behind.
The commitment: measured, not just promised
Every technology vendor doing a system migration will tell you your data is safe with them. That's the easy part to say and the hard part to prove.
The commitment at the center of this engagement was specific: every donation record that existed in the old system would exist — with every field that mattered — in the new one. We call this the parity guarantee, and the word that matters most in that sentence is guarantee, not as a promise we made, but as a number we produced. Before we called the migration done, we compared the old system and the new system directly, every row, and can show the result.
What we built
The rebuild itself is a modern web application — a separate front end and back end, built so the part of the system that manages donations and the part that manages donor relationships stay cleanly separated from each other. That separation matters practically: a change to one doesn't put the other at risk, and the system can grow in either direction without the whole thing needing to be re-touched.
It ships with an automated test suite that runs the application through its paces on every change: 835 tests covering the core system (4,944 individual checks within those tests), a further 246 tests covering the interface, and 4 tests that walk through complete user workflows end to end. All of them passed at delivery.
The foundation's staff log in through the identity system they already use. We built that connection as a swappable piece, so when the foundation finishes deciding exactly which staff should get which access level — a decision that was still theirs to make at delivery — turning it on is a configuration change, not new code.
The migration itself was engineered, not just run
The riskiest moment in any system replacement isn't the new code — it's the night you move the real data over. We built a dedicated, one-command migration tool for that moment, with three properties that matter to anyone who has to sign off on it:
- It's atomic. If anything goes wrong partway through, the entire operation rolls back and the target database is left untouched — no partial migration, no "we'll clean it up after."
- It's safe by default. The tool refuses to run against a database that already has data in it unless someone deliberately confirms they mean to override that.
- It corrects, not just copies. Rather than moving the old records over exactly as they were, the migration checks the schools directory against the district's current, authoritative source and fixes what had drifted: 36 name corrections, 11 missing school records restored, and duplicate records resolved. A naive migration would have copied the old, stale version forward. This one copies forward what's actually correct today.
Only one donation-level change happened anywhere in the migration: three donations tied to a school that had physically relocated were reassigned to the surviving record for that school. That's it — the only change to a donation record in the entire operation, done as a deliberate, logged, and separately verified step, not a side effect.
How we proved it
Proof, here, means three separate checks, run in sequence, each one independent of the last:
First, 21 behavioral checks covering the entire donation workflow — record counts, exact field-by-field comparisons, school lookups, exports, letter and label generation, and the seasonal lock that prevents changes outside the giving period. 21 passed. 0 failed. 0 incomplete.
Second, a full data comparison, not a sample. A database engineer loaded the old system's data and the new system's data side by side and ran a column-by-column comparison across every donation record — not a spot check, the entire set — on every field that mattered. The result: 0 mismatches. Not "close." Not "materially the same." Zero. Separately, exactly 27 records carried a special "amount unknown" flag in the old system, and all 27 came through with that flag intact.
Third, someone else checked our work. Our QA function reran the same comparison independently, from scratch, against the same data, and got the same numbers. That independent, reproduced result is what let us call the migration formally accepted — not because we said so, but because two separate checks, run separately, agreed.
The one place the new system's numbers intentionally differ from the old one is the schools directory itself — because we corrected it against the current authoritative source, as described above. The donation totals never move.
What the foundation has now
- The application's source code, delivered directly into the foundation's own code repository, ready to deploy. They own the deployment pipeline; we don't sit between them and their own system.
- The one-command migration tool, with a written runbook, a documented safety model, and a built-in verification flag they can re-run any time.
- Four separate handover documents, each written for a different reader: day-to-day office staff, IT operators, the foundation's future developers, and our own internal reference — none of them assembled from a spec, all of them checked against the actual, running application.
- A working system with four access levels (system administrator, school administrator, department administrator, and read-only), where each user only ever sees the data for their own school — enforced by the system itself, not by policy. A seasonal freeze mode prevents changes outside the active giving period. And a donor and company management surface that the old system never actually had, despite being asked to do that job.
What this says about legacy-system risk generally
The tempting way to scope a project like this is to call it "a rewrite" and treat the data move as an afterthought — build the new thing, copy the data across, hope it lines up. The expensive lesson in system migrations is that the data move deserves the same engineering discipline as the application itself: its own design, its own dedicated test harness, its own pass/fail criteria, checked independently before anyone calls it done.
The schools-directory correction is the clearest example of why that discipline pays off. Copying the old records forward unchanged would have been the easy path — and it would have quietly carried old, uncorrected data into a system meant to outlast it. Cross-checking against the current source of truth instead means the system that goes live is accurate as of today, not accurate as of whenever the old system was last updated. That distinction is exactly why the identity system will map staff to the right school when the mapping goes live: it depends on the same directory that migration went to the trouble of getting right.
If your own systems are carrying that same kind of risk — old, undocumented, load-bearing, and nobody quite sure what would happen if you touched them — the question isn't whether they eventually get rebuilt. It's whether, when they do, you'll be able to point to a number and know nothing was lost.
Stay updated on BitSalt’s work
Get notified when new tools launch and new posts go up. No drip sequence.