Cemetery records. Moving a hundred-year-old cemetery’s records off paper.
A Board Member Shouldn’t Have to Go Find the Book.
The board needed member, plot, and ledger records on their phones. I also had to get burial availability right: a plot can hold more than one kind of arrangement.
Could a volunteer answer a family’s question from the cemetery?
Executive perspective
Why does this matter?
The records needed to be available at the cemetery.
I built a public site and a board portal so volunteers could look up members, plots, and the ledger from a phone. When a family asked about a plot, a board member could get the answer without going back to find a book.
Occupied or empty wasn’t enough.
A plot could hold different burial arrangements. If I got those rules wrong, the system could tell a family there was room when there wasn’t. I had to understand the arrangements before deciding how to store them and calculate the remaining space.
The next board needed to be able to use it.
Shared records and role-based access gave volunteers a consistent place to work. Administrative records could be archived and restored after a mistake. Burial records needed stronger protection: deleting a record must never make occupied ground appear available.
Architecture perspective
Why was it designed this way?
I didn’t want availability entered twice.
A separate occupied-or-empty field could get out of step with the burial records. I calculated availability from the assigned burial positions instead. A full burial, a burial with a cremation at the foot, and multiple cremations have different rules. Those assignments determined what space remained.
Deleting a member and deleting a burial aren’t the same.
Administrative records could be archived and restored. Burial records permanently blocked the positions they occupied. I couldn’t use one deletion rule for both without risking a wrong answer about the ground.
The permission rules needed to agree.
The public site and board portal shared a database. Roles controlled what board members could do. My review found that the documentation, API, and interface described different permissions. I brought them into agreement.
Technical perspective
How does it actually work?
The site used Azure services.
Azure Static Web Apps served the applications, Azure Functions handled the API, and Azure SQL held the records. The recorded repairs included nine guarded migrations and a nineteen-test integration suite.
A successful request didn’t mean the email arrived.
The detailed account records email failures behind successful HTTP responses and an authentication-state assignment that worked locally but failed in production. I found those problems in production telemetry after the local checks passed. The account below keeps the details and the limits of what I checked.
The detailed project account
These details come from the account written before the September 2026 site update. I’ve rewritten the wording, but kept the measurements, findings, and qualifications. “Today” and other time references describe that earlier period. I haven’t remeasured the results or assumed that open issues were fixed.
What was needed
The cemetery had operated for more than a hundred years using paper records: ledger books, plot maps, and index cards. Later, board members also emailed spreadsheets back and forth.
The records were there, but a board member at the cemetery couldn’t always get to them. If a family called to ask whether there was space beside a grandmother’s plot, someone had to go find the book. I wanted the board member with the phone to be able to look up the answer.
The board was made up of volunteers, and volunteers changed. A new member might not know where to find things. I needed the records to be available to whoever had the question, including on a phone at the cemetery.
What I built
I built two applications sharing one database: a public site for families and the community and an administrative portal for the board. The portal held member records across four categories, plot and burial-position assignments, and a financial ledger. Roles controlled the work available to the treasurer, secretary, and officers.
The burial rules took the most care. A plot could hold a full burial, a full burial with a cremation at the foot, or up to four cremations in defined positions. The positions already occupied determined what remained available. If I got that wrong, a family could be told there was room when there wasn’t.
I calculated plot status from the burial assignments instead of asking a volunteer to update another field. Burial records permanently blocked the positions they occupied. There was no soft delete or archive path that could make occupied space look free.
Other records could be archived and restored. Volunteers make mistakes, and I didn’t want a wrong click to permanently remove the association’s records.
How the board used it
Board members used it from their phones for member records, plot lookups, burial positions, and the ledger. I designed around that use: in the cemetery, at a meeting, or on a call with a family.
The public site delivered messages from families and the community to the board.
The next board would have the same place to find the records.
What I found when I checked it
I audited and repaired the system in two days with eleven parallel AI workers managed by a tool that ran checks on their work. Each worker owned separate files. Before merging, each task had to pass an executed command. If it failed, the next attempt received the failure output.
The audit found several problems that could interfere with the board’s work:
The public contact form logged submissions instead of sending them. It thanked the sender, but the board didn’t receive the message. The page gave no indication that delivery hadn’t happened.
The documentation, API, and interface described three different sets of permissions. Board members could see buttons for actions they weren’t allowed to complete.
Member deletion was permanent. The ledger’s foreign key had no cascade rule, so trying to remove a member with financial history returned a server error. Depending on the member’s history, deletion either lost the record permanently or failed.
Sessions expired after thirty minutes. The API had a refresh endpoint, but the front end didn’t use it, so volunteers could lose work during data entry.
Half the production database schema wasn’t in version control, and real member data had been committed to the repository.
I closed those findings with nine guarded migrations, consistent permission rules, soft deletion for administrative records, derived plot status, a nineteen-test integration suite, and working email delivery. All nineteen tests passed against production. I attached the verification results to each issue.
The tests also found something the audit missed: a production plot-type constraint contained values meant for an entirely different kind of data.
I didn’t find either problem in code review or the local checks. I found them by querying Application Insights after deployment. That was why I kept checking after the deployment succeeded.
What I learned
-
I calculated availability from burial records because I didn’t want a volunteer to have to remember a second update. The answer about space needed to follow the assignments.
-
HTTP 200 didn’t prove the message was sent. I had to look for the missing result: the null timestamp, empty queue, or email that never arrived.
-
In this work, Azure SQL’s firewall stalled connections from unlisted addresses instead of rejecting them. The migration script hung without an error message. That made a firewall problem harder to recognize.
-
I stopped storing authentication state on the Azure Functions request object. Its existing
userproperty made that assignment a silent no-op on the deployed worker, even though it worked locally. I used a local variable instead. -
I wrote tests after the audit. They found a defect the audit had missed.