You deleted the student, and the next roster sync put them back
In short
A student who comes back after deletion was either re-created by the upstream roster, never removed from a downstream copy, or restored from a backup. Probe every store twice — once immediately after the delete, once after the next scheduled sync — because a row present at both times was never deleted, while a row absent then present was re-created.
Key takeaways
- Two probes separate the causes: gone-then-back is re-creation, never-gone is an incomplete delete.
- A tombstone holding a salted hash of the source id lets the importer skip a returning row without keeping identifiers.
- Derived stores — indexes, caches, warehouses, embeddings — need their own deletion path, not a cascade you assumed.
- Backups make deletion asynchronous by design. Say so in writing, with the maximum window, and re-delete after any restore.
- Suppression is not deletion, and calling it deletion is the part that breaks trust with a school.
- Deletion is a testable behaviour. Seed a student, delete, and assert not-found in every store in continuous integration.
A student who reappears after being deleted has taken one of four routes back, and the fix for each is different: the upstream roster re-created them on the next import, a downstream copy never received the deletion, a restore brought the row back, or a derived artefact still holds the content under a different key. Running the delete job again resolves none of them, and it hides the evidence you need.
So the first job is a measurement rather than a remediation. Deletion in a system with an upstream source of truth and half a dozen downstream copies is a distributed operation, and the question worth answering is which participant did not play its part. Two probes settle it.
The probe: check every store twice
Delete one test student, then run an existence check across every store you own — immediately, and again after the next scheduled import. The pattern across those two checks names the cause without any further investigation.
- Enumerate the stores first: Postgres or whatever the primary database is, the search index, the cache, the warehouse, the vector store, object storage holding exports and generated documents, queues with unprocessed events, and any device-side copy. On a typical build that is 7 or 8 destinations.
- Record the student's identifiers in every form they appear — internal id, source system id, email, and any hashed variant — because a store that missed the delete usually keyed on a different one.
- Run the delete through the same path a real request would take, not through a console. A console delete tests a code path nobody will use again.
- Probe every store immediately. Anything present now was never deleted, and that is an incomplete-deletion problem confined to that store.
- Wait for the next scheduled sync — most run nightly, between 01:00 and 03:00 local time — and probe again. Anything absent at the first probe and present at the second was re-created by an upstream import.
- Record both results per store in a table. It is the evidence a school's reviewer will ask for, and it is faster to produce now than to reconstruct after the second complaint.
| Store | How to probe it | Present at both probes | Absent, then present |
|---|---|---|---|
| Primary database | Query by every identifier form, including soft-delete flags | The delete never ran, or it only set a flag | The importer re-created the row |
| Search index | Query OpenSearch or the index directly, not through the app | The index delete was never wired up | The reindex job rebuilt the document from a stale source |
| Cache layer | Read the key directly in Redis or the equivalent | No invalidation on delete | A read-through cache repopulated from a copy that still has it |
| Warehouse or analytics copy | Query the fact and dimension tables | The pipeline propagates inserts and updates but not deletes | The next load re-inserted the row from an unfiltered extract |
| Vector or embedding store | Search for the content, not the id | Embeddings were never given a deletion path | A re-embed job read a source copy that still contained it |
| Object storage | List by prefix in S3 — exports, generated PDFs, uploads | Files are not addressed by student, so nothing deleted them | A scheduled export regenerated the file |
| Upstream roster source | Look at the file or API payload the sync consumed | The student is still enrolled upstream, which is the real answer | The student was re-added upstream, deliberately or not |
Re-created by the upstream roster
This is the most common answer, and it is not a bug in your importer. The student information system is the system of record; your product holds a projection of it. If the student is still in the feed — through OneRoster, a connector such as Clever or ClassLink, a direct pull from PowerSchool or Banner, or a nightly file drop — then every import correctly re-creates what the source says exists. The transport itself is a separate subject, covered by direct SIS pull versus a managed roster sync.
The design answer is a tombstone the importer consults before it creates anything. Keep the tombstone minimal: a salted hash of the source system id, the deletion timestamp, the requesting institution, and the reason class. That is enough to recognise a returning row and refuse it, without retaining the identifiers you were asked to remove — and the salted hash matters, because an unsalted one over a small, guessable identifier space is not much protection, for the same reason set out in why removing names does not de-identify a class of eleven.
- Decide who is allowed to lift a tombstone, and make it an explicit action with a recorded reason. Students legitimately return — a withdrawal reversed, a transfer that fell through.
- Surface refused rows in the sync report. A silent skip looks identical to a broken integration when the school's data lead is counting seats.
- Distinguish deletion at the institution's request from a student simply leaving. They have different triggers, different clocks and different reversibility, which is the distinction retention clocks per record type exists to hold.
- Watch what re-creation does to access. A resurrected student reappears in section membership and caseloads, which quietly re-grants permissions — a variant of staff being able to see students outside their caseload.
Never deleted: the copies that never got the message
The second family is quieter, because nothing reappears in the interface. The row simply never left one of the derived stores, and it surfaces later in a report, a search result or an export.
- Warehouse and analytics pipelines that replicate inserts and updates but treat deletes as an edge case. Append-only loading into Snowflake or BigQuery is the usual shape, and it needs an explicit delete propagation rather than an assumption.
- Search indexes and caches, which are rebuilt from sources rather than mutated, so a delete has to be a positive action against them.
- Embeddings and any fine-tuning corpus. Content that entered a training or retrieval pipeline needs its own removal path and, for training in particular, may not be removable at all — which is why the decision about what may enter one has to be taken first: can student work train the model, and where the line sits.
- Generated artefacts: PDF reports, exported spreadsheets, emailed attachments and scheduled dumps sitting in object storage under a filename nobody indexes by student.
- Copies on devices. A spreadsheet a teacher exported and a cached record inside an app on a personal handset are both outside your deletion path entirely — the exposure described in staff using personal phones for school work.
Restored: backups, and the honest way to describe them
Backups reintroduce deleted rows by design, and no architecture removes that property. Rewriting historical backups to excise one student is both technically fragile and self-defeating, because it undermines the integrity the backup exists to provide. The workable position is that deletion from backups happens by expiry, and any restore is followed by a re-application of the deletion log.
Suppression or deletion: pick one deliberately
Both are legitimate. Calling one by the other's name is not, and it is the specific thing that costs an institution's trust when it comes out.
| Property | True deletion | Suppression |
|---|---|---|
| What remains | Nothing beyond a minimal tombstone | The record, hidden from all interfaces |
| Survives a re-import | Yes, if the tombstone is checked | Yes, and the data is refreshed while hidden |
| Reversible | No | Yes, which is sometimes exactly why it is chosen |
| Defensible answer to did you delete it | Yes | Only if you called it suppression from the start |
| Right for | A deletion request, or the end of a retention clock | A short reversible window, or a legal hold |
Deleting a student is not a statement about one table. It is a claim about every copy, including the ones built by people who were not in the room when the request arrived.
What to build so this is answerable next time
- A deletion service, not a delete button. One entry point that fans out to every store, records what it touched, and reports partial failure loudly instead of returning success.
- A store registry that the deletion service iterates over, so adding a new store means registering a deletion handler rather than remembering one.
- A tombstone table with a lifting procedure and an owner.
- An automated test in continuous integration: seed a student, delete them, assert not-found across every registered store, and fail the build if any store answers otherwise. This is the check that keeps the guarantee true after the fourth new data pipeline.
- A deletion receipt for the institution: what was deleted, when, from which stores, what is suppressed rather than deleted, and the date the last backup copy expires.
That receipt is usually the most valuable artefact in the whole exercise, because it converts an argument into a document — and building the fan-out, the registry and the report is exactly the kind of narrow internal system we scope under internal tools and ops. Where transcripts and logs fit into the same deletion path is covered separately in a records request that came back with assistant transcripts. The rest of this silo sits under student data privacy, engineered, within our education and edtech work.
Frequently asked questions
Short answers to the follow-ups this page tends to raise.
Why do deleted student records reappear after a roster sync?
Because the student information system is the source of truth and your product holds a projection of it. If the student is still in the feed, the next import re-creates them, correctly. The fix is a tombstone the importer checks before creating a row — not a second delete, and not disabling the sync.
Should student records be soft deleted or hard deleted?
Hard delete for a deletion request; soft delete only as a short, clearly labelled reversible window. A soft delete leaves the record present in the database, in backups and often in derived stores, so describing it to a school as deletion is inaccurate. If reversibility is genuinely needed, call it suppression, document it, and give it an expiry after which the real deletion runs.
How do we handle student data deletion in backups?
By expiry, with the window written down. Rewriting historical backups to remove one student is fragile and undermines their integrity, so the defensible position is that backups age out on a stated schedule and any restore is followed by re-applying the deletion log. Tell the institution the maximum window in plain terms rather than implying deletion is instantaneous everywhere.
How can we prove a student's data is actually gone?
With a per-store existence probe and a deletion receipt. The probe queries every registered store by every identifier form and records the result; the receipt lists what was deleted, when, from where, what is suppressed instead, and when the last backup copy expires. Run the probe as an automated test so the proof stays true after the next pipeline is added.
- student privacy
- data deletion
- roster sync
- data architecture
The work behind this page
Builds from our portfolio that this page draws on.
AskVault
An AI internal knowledge-search platform that answers employee questions from your own docs — grounded in citations, with knowledge gaps surfaced and deflection tracked.
Productivity AIFocuscare
End-to-end physiotherapy consultation automation: patient onboarding to AI-generated notes and follow-up scheduling.
Healthcare AIRead next
- A records request came back with assistant transcripts nobody could redactThe transcripts were never classified, so nothing was designed to filter or delete them. The test is not which system wrote the row — it is whether any join you control gets from that row to a named student.diagnostic
- After the term rollover, staff can see students who are not theirsThe role model is probably fine. What is missing is the moment where an ended assignment removes an access that was granted when it began — and rollover is when a year of those omissions becomes visible at once.diagnostic
- A student data privacy agreement, read as an engineering specificationSchools bind vendors with a standard agreement plus an exhibit naming every data element collected. Five of its clauses are build work, and the exhibit is a schema diff in disguise.definition
- Directory information is a per-student flag your code has to honourSchools designate their own directory information list, and any family can opt out mid-year. That makes it a dated per-student flag every outbound surface reads at render time.definition
- The school official exception, and what it obliges a vendor to buildA vendor holding student records stands inside the school's own permission, not outside FERPA. Each condition of that permission converts into something you build and evidence.definition
- Verifiable parental consent, and when the school can give it insteadUnder COPPA, consent must be obtained by a method reasonably likely to prove it came from a parent. Schools can stand in for parents in narrow classroom cases, and the deliverable is a consent record.definition
Working on something in this space?
Tell us where you are in a sentence or two. We'll tell you honestly whether we're the right team, and what a sensible first slice of the work looks like.
Start the conversation