What was going wrong
Before a provider can bill an insurer, the insurer has to approve them. That approval is credentialing, and each one, one client on one insurer, is called a panel. It is where a growing practice group quietly loses money, and here it lived in a workbook: one row per provider per insurer, a color for status, a date somebody typed by hand. Nobody could answer the two questions that decide the month: which applications are waiting on the team, and which are waiting on the insurer. Follow-ups happened when someone remembered.
Around that sat separate tools for records, billing, messages, and compliance, where the handoffs caused the errors and the compliance tool was the one nobody opened. Money was messy too: staff are paid in Pakistani rupees, invoices go out in US dollars, and both sides have to add up. AI could save real time here, but dropped in carelessly it puts patient data at risk.
What I did
The first decision was a boundary: patient data does not live in this system, and the code enforces that, not a policy document. Every free-text box is checked for patient details before anything is saved, and every AI request carries a standing instruction to keep them out.
Inside that boundary the unit of work is simple: one client, one insurer, one application. Status is worked out from the application itself, never typed twice. Each insurer has its own follow-up timetable, and each person starts the day with their own list, most overdue first, instead of a shared sheet.
Every morning the system screens against the US government's list of excluded providers, all 83,665 rows, and flags any possible match. Every amount of money is stored with the exchange rate it was recorded at, so a report run again later matches the original line for line. Finance and credential records are kept for 7 years, everyday records for 1.
What changed
Live on the client's own domain since June 2026, used every day by five credentialing staff. By the end of July 2026 it held 250 clients and 2,042 applications, with 13,702 logged events behind them, in one system instead of a workbook.
And it answers questions the workbook never could. At the end of July 2026, 125 applications were waiting on the team and 1,560 on insurers. 147 coverage gaps had turned up, and 21 were active then: the provider is approved on an insurer's medical side but not its behavioral health side, so those claims were being denied, and the denials looked like billing errors.
By then the insurer directory covered 66% of active applications, up from 34%, and the follow-up list had gone from nothing to about 60 due a day. Tests went from 185 to 448 in July 2026, and the 26 passwords and codes that sat unencrypted on the live system were all encrypted.
What's running today
Credentialing is the spine. A panel is one client on one insurer, which is one application, and everything in the module hangs off it. Five credentialing staff work out of it daily, on the client's own domain.
Getting there was not a data problem, it was an adoption problem, and a check against the live data made that concrete: 0 of 1,066 panels had a follow-up date, no client was assigned to a named owner, and there were no insurer contacts on file. The module held the right data and nobody's day started in it.
The fix was not more fields. Each person got a "my day" list, most overdue first, with an inline log that reschedules the row and clears it: the sticky note the team lead can also see.
A call kit puts the insurer's phone line, their portal, the reference number, the expected turnaround and the last three touches on one screen, so a chase call takes one tab instead of six. Availability is tracked per insurer and per state, so an application that is doomed before it is filed can be caught before it is filed.
Around that sits the daily ops layer, which was built first: a work-from-home, office or field check-in on the first visit of the Karachi day, leave with pro-rated balances for new joiners, credential expiry alerts at 90, 60, 30 and 7 days, team invites by role and region, and an activity feed where an admin can undo an allowed change in one click. Every reminder writes a log row, so nothing sends twice.
The finance module is the part I am most pleased with. It runs salaries, expenses with receipts, payroll, bonuses, and a monthly invoice that forecasts the next month and then reconciles against what was actually spent. Invoices print with US dollar and rupee columns side by side, and the people who pay them get their own dollar-only view where they can acknowledge an invoice, raise a query, or mark it paid.
What broke
Four things went wrong that are worth writing down. They are the reason a signed-in walkthrough is now its own step in my process, not something I assume the tests cover.
The original import silently dropped a third of the source. The workbook migration looked clean and was not. The workbook held 240 provider blocks on 141 tabs. Only 141 of those blocks came across, and only 1,123 of its 1,796 insurer rows. The recovery on the live system took clients from 194 to 250 and panels from 1,521 to 2,042. It also showed that the credential-expiry reminders had had nothing to fire on since go-live, because zero credential records had come across at all. Two judgment calls became locked decisions during the recovery, because the obvious move was wrong both times: red in the sheet does not mean closed (mapping it automatically would have killed around 240 live panels), and a continuation block that repeats the first block's provider number is the same person, not a new client (importing all of them would have created 42 duplicate clients).
A screen that was dead, and no test could see it. The panels page showed its loading state forever: no request ever made, nothing in the console, tests green. It only turned up in the first signed-in walkthrough the module had ever had. That same walkthrough caught a median arriving as -1 day, from mixing a date column with a date-and-time one.
Real passwords were sitting in plain text on the live system. Around 26 rows of a sign-in hint field held insurer portal passwords, recovery codes and the secret key for a sign-in code app, pasted there by staff as notes, and the call kit I had just built would have shown them on the busiest screen in the module. The screen now hides anything that looks like a password and offers a logged reveal instead, 45 recovery codes moved into encrypted storage, and the remaining rows were encrypted too, so the count is zero. None of that arrived as a bug report. It came from opening the screens the way a user would.
A re-compile that could have erased a sent invoice. The invoice compiler deleted the previous document before picking the next number, so a re-compile handed back a number that was already out. The delete had no status filter either, which means a re-compile after sending would have erased an invoice a client was holding. Caught before it happened. Now the delete skips anything already sent, numbers only go up, a replaced invoice is kept rather than deleted, and the page warns when an invoice is out of date.
Decisions I'd defend
A panel is the unit of work, not a provider. One client with five insurers is five rows, each with its own status, chase date, reference and history. It reads as duplication at first glance, and the alternative (one row per provider with the insurers stuffed inside) is what made the workbook unanswerable. Same-named clients are told apart in the list by license state, then source tab, then the end of their provider number, and any detail that comes out identical across the group is dropped rather than printed as a label that tells nobody anything.
Insurer names have to match exactly, on purpose. Insurer names on panels arrive as free text, and a chunk of them do not match the directory. They stay unmatched until a person links them, because filing an availability fact against the wrong insurer is worse than filing none. Guessing at near matches would buy a fuller dashboard and a wrong answer.
The patient-data boundary lives in code, not in a policy document. The check on every free-text box and the instruction on every AI request are independent on purpose: if one misses, the other still holds. Calling the AI any other way counts as a bug, and the notes for anyone working on the code say so. Compliance arguments stop once the rule lives in code.
Money reports never look up today's rate. A report regenerated from yesterday matches the original row for row, even if the rate moved overnight, because each amount carries the rate from the day it was recorded. That is the difference between roughly right and audit-ready, and it costs almost nothing to enforce.
Two record-keeping tiers, set in code. Finance, credentials, employee contracts and payments are kept for 7 years. Leave, work from home, notifications and sales activity are kept for 1 year. The tier is written at every place the system logs an event, so future-me cannot quietly downgrade a finance record by picking the wrong one.
The salary calculator takes the slow, safe route. Working back from take-home pay to a gross salary has a fast shortcut that is right most of the time. It goes wrong when a bonus pushes a salary into a new tax band, and the answer drifts by about 10k PKR. The slower method cannot drift that way, and a note in the code records the example so nobody swaps the shortcut back in.
Invite-only signup. There is no public sign-up page. Admins create users, the invitee gets a one-time email link, sets a password, and the account goes live. Self-serve signup has no value for a clinic operations platform, and it has a real cost: a bigger surface for patient data to leak through.
What's next
The availability pre-check already exists and returns a plain-sentence answer per insurer and state: whether that insurer is open to new providers, and when that was last checked. Nothing uses it yet. Wiring it into bulk enrollment is the next change that moves a number, because it stops doomed applications before they are filed. After that: de-duplicating people by linking their records instead of merging rows, linking the insurer names that do not match the directory yet so the availability map fills in, and naming practices from their legal business name instead of whichever member happened to anchor the group.
If your credentialing or billing team has outgrown the workbook and the shared inbox, tell me what it looks like. The credentialing core, the record-keeping, and the finance engine are reusable now, so your build starts further along than this one did. And because it handles two currencies, a US company and a team in Pakistan can work in the same system.

