A lot of institutions are running a Student Information System that predates most of the people using it. On one campus we worked with, the SIS had been in place for decades, and everything built since had been built the way things were built at the time: inside the vendor platform, with custom tables added alongside the vendor's own, on servers in a room down the hall. Years of one-off tools, reports, and utilities that departments used daily and that worked.
That's not a story about anyone making bad calls. It's a story about what was available. When those decisions were made, there was no in-house web development practice to build against, no cloud to move to, and no particular reason to think the vendor platform was a risky place to put things. Each layer got added on top of the one before it because that was the surface that existed. Anyone who has worked in higher ed IT, or in any institution older than the software it runs on, has watched the same accumulation happen.
What had changed was the horizon. A cloud SIS migration was coming. Custom tables sitting in a vendor's database and tools living inside a vendor's interface don't travel well, and the closer that migration got, the more expensive they'd be to unwind. The question wasn't whether to deal with it. It was whether that work would happen under a go-live deadline or ahead of one.
So we built a toolbox. Blazor front end, ASP.NET Web APIs behind it, SQL Server underneath, authorization driven by the directory groups the institution already maintained. All of it running on prem, because that's where the institution was — and all of it built so that on-prem was a deployment target rather than an assumption baked into the code.
That last part drove most of the technical decisions. The front end talks to APIs over HTTP rather than reaching into a database directly. Environment specifics live in configuration rather than in the code. Data access goes through the API layer instead of being scattered across the UI. None of that is exotic, and all of it is ordinary practice anywhere that's been doing web development for a while. It mattered here because the same application running on a server down the hall could run somewhere else later without being rewritten — which meant the institution wouldn't have to make the cloud decision and the rebuild decision at the same time.
We split the API layer in two: one for internal tooling, one for vendor integrations. Those are different trust boundaries, and they earn different surfaces. Serving both from a single API with route-level filtering would have saved setup time and cost more later, because every endpoint added afterward would have had to correctly identify which side of the boundary it belonged on. Two separate APIs makes that boundary structural rather than something a developer has to remember — the separation is enforced by what exists on each surface, not by convention.
Authorization leans on the directory the institution already ran. Group membership determines which tools a person sees, with separate administrative groups layered on top. The useful consequence is that access isn't managed inside the application at all — it's managed where it already was, by the people who already did it. Granting someone access to a department's tooling is a group membership change rather than a ticket to our team, and access ends when their account does. Homegrown permission systems usually get the granting right and the revoking wrong.
SQL Server holds everything, but in a database of the institution's own, rather than as additional tables inside the vendor's. That distinction is most of the point. Same data, same server room, but on a footprint the institution could back up, migrate, or restructure on its own schedule.
The other decision we'd make again on day one was saving the front end and the API as templates in source control. A new front end or a new API starts as a fork instead of a blank project. The time savings are real but secondary — what matters is that the tenth thing built on the platform has the same wiring, the same structure, and the same conventions as the first, whether or not the same person is building it. Platforms decay when every addition is a slightly different reading of the pattern, and making the pattern forkable is the cheapest defense against that we know.
Two goals drove the whole effort. The first was tools that belong to the institution: independent of any one vendor's platform, able to survive a migration, changeable on the institution's own schedule. The second was laying groundwork for automation across systems, and the first is a prerequisite for the second. Automating across systems is difficult when the automation logic is a tenant inside one of them. Once tools sit on infrastructure the institution controls, with its own APIs and its own data layer, automation becomes a matter of composing things the institution already owns.
This was never a rewrite-everything project, and it shouldn't have been. The older tools work, and departments depend on them. The toolbox started with what hurt most and grew from there. What changed immediately wasn't how much had moved. It was that there was finally somewhere else for the next tool to go.