muhaiman.dev
Engineering4 min read

Rebuilding a Reporting Engine Inside a Legacy SaaS

Published on

When engineers talk about refactoring or building major features, they usually imagine modern setups: TypeScript, Next.js, an ORM with type-safe schemas, and a staging database you can reset whenever you want. Real production work is rarely that neat.

When I joined CountBubble, an established SaaS platform that human-services nonprofits rely on for daily case management, I stepped into an active system with years of production history. People used it every day to coordinate care, track participant outcomes, and manage compliance records. Breaking anything was not an option.

The technology stack had none of the conveniences of modern greenfield projects. The frontend was built on React 16 with a custom internal component library and manual state handling. The backend was untyped JavaScript running on AWS SAM (Serverless Application Model), coordinated locally through Express and Docker. Instead of an ORM like Drizzle or Prisma, the data layer used a mediator pattern. All database queries were written as raw SQL across large repository files, and database migrations were tracked manually through numbered SQL files.

To make things more interesting, I did not have direct access to production database volumes. Because the system was actively serving real nonprofit organizations, I could not just inspect live databases or run exploratory scripts against production data. Every schema change and every query update had to be tested and verified locally, ensuring that nothing broke backward compatibility or slowed down serverless execution.

Before tackling anything large, my first task was building a participant assignment feature. Nonprofits needed the ability to link participants directly to one another so case managers could coordinate oversight across related individuals. The feature touched the entire stack: new serverless endpoints, raw SQL queries in the mediator layer, and updates to the React 16 frontend. Building that first feature gave me a grounded understanding of how data moved through the platform and how to safely update tables without disrupting active records.

Once that was running smoothly, I took ownership of the reporting engine.

The existing reporting section was a basic, single-page view. A user could apply a few filters and look at numbers, but there was no persistent state. If a case manager spent ten minutes configuring column selections and date ranges, and then accidentally refreshed the page or stepped away, the entire setup disappeared. For organizations that run regular weekly and monthly audits, repeating that manual work was a constant source of friction.

I decided to turn that flat screen into a full reporting workspace.

The first step was building a persistent navigation sidebar. Users needed a way to search through saved reports, categorize them, and reload them instantly with their original filters and column layouts preserved.

The harder part was handling changes to existing reports. If someone loaded a saved monthly report and tweaked a date range, should it overwrite the original report right away? What if they were just experimenting with numbers? Overwriting immediately would ruin saved configurations, but asking users to confirm every tiny click is annoying.

To solve this, I built a multi-stage draft lifecycle. When a user changed any filter parameter on a saved report, the system quietly created an isolated draft state. The user could inspect the updated dataset without affecting the original report. Once they were satisfied with the results, they had two explicit choices: apply the updates back to the parent report, or branch the changes off into an entirely new report. This gave staff the freedom to explore data without fear of losing their original setups.

As organizations saved more reports, another problem showed up: running them one by one was tedious. An agency often needed five or six related reports for a single monthly audit.

To handle that, I built report grouping and batch execution. Users could group related reports under shared operational categories, like family services or quarterly housing assistance. Instead of clicking into each report individually, a manager could trigger a batch run for the entire group, applying shared global filters for date ranges and participant groups in one go.

Working on CountBubble for an extended period gave me a deep appreciation for production discipline. When you build from scratch, you can pick whatever shiny libraries you like. When you work on a live system, you learn to respect existing patterns, think twice before touching dependencies, and write clean SQL that behaves predictably under real data loads.

We shipped the entire reporting workspace to active nonprofit users without causing a single production regression, and that made the careful work completely worth it.