Notes from a v1 to v2 rewrite — what happens when you build a system that can do anything and nobody can explain what it does.
We’re in the middle of rewriting our platform at Superkey. Not a refactor — a rewrite. v1 worked. It also made everyone’s life harder than it needed to be, in ways that took us a year to fully understand.
This is the postmortem, written while we’re still building v2.
When we started building, we didn’t understand the domain well enough to be opinionated. Insurance workflows are complex — different association types, different carrier requirements, different regulatory constraints per state. The safe-seeming choice was to build for flexibility: configurable workflows, generic data models, APIs that could handle any shape of submission.
The logic was: if we make everything flexible, we won’t have to rewrite when we learn more.
We were wrong in a way that took a year to become obvious.
When you don’t know what the right abstraction is, you avoid committing to one. Our API surface grew organically — endpoint per feature, shaped by whoever requested it that week. Six months in, we had dozens of endpoints with inconsistent naming, inconsistent error handling, and no shared patterns for common operations like status transitions or document attachment.
The cost wasn’t technical debt in the usual sense. It was cognitive debt. Every new developer who joined had to learn 30 slightly different ways to do the same thing. Every AI agent we pointed at the codebase had to be told “no, not that endpoint — the other one.”
We had our platform, our association management system, and a handful of carrier portals. The boundaries between them were never clearly defined. Where does a submission live — in our system or the AMS? Who owns the policy status after binding? When a carrier sends back a quote, does it update our database directly or go through the AMS first?
The answer was “it depends,” which is another way of saying “nobody decided.” Different workflows pushed data in different directions. Some state lived in our system, some in the AMS, some in both with no reconciliation. Every integration was a special case.
Insurance platforms serve multiple personas with genuinely competing needs. The underwriting team wants more fields on submissions. The customer-facing team wants fewer. The compliance team wants everything logged. The operations team wants streamlined workflows that skip steps. The board wants dashboards.
We handled this by adding configuration. Need a field to be optional for one persona and required for another? Add a config. Need a workflow step that one team skips? Add a toggle. Need a dashboard that shows different metrics per role? Add a view layer.
Configuration compounds. Every toggle doubles the number of system states you need to test. Every conditional field is a branch in your validation logic. Every persona-specific workflow is a code path that has to be maintained independently. Within a year we had a system that could, in theory, serve every persona — and in practice confused all of them.
The breaking point wasn’t a technical failure. The system worked. It just took too long to do anything in it.
Adding a new carrier took weeks instead of days because the flexible workflow engine needed to be configured for their specific requirements. Onboarding a new association required a support call to explain which of the 50 settings they should turn on. Bug reports came in that were really configuration misunderstandings — “the system is doing the wrong thing” meant “the system is doing what it was configured to do, which isn’t what anyone intended.”
We were spending more time managing the system’s flexibility than using it to serve customers.
The single biggest change: our platform is the system of record for everything in the workflow. Not the AMS, not the carrier portal — us. If data needs to flow to another system, it flows out from ours. If data comes in from another system, it comes in through a defined integration point with a clear owner.
This meant subsuming capabilities we’d previously delegated to the AMS. That’s more code to write, but it eliminates the “where does this data live?” question permanently. One source of truth. One place to query. One place to debug.
v2 has half the configuration surface of v1. We made decisions. Submissions have a specific set of required fields. Workflows follow a specific sequence. The status machine has defined states with defined transitions.
The teams that wanted different things? We put them in a room and forced decisions. Not consensus — decisions. “This is how the workflow works. If your persona needs something different, we’ll discuss whether that’s a real requirement or a preference.” Most of the “requirements” turned out to be preferences.
This was uncomfortable. It felt authoritarian. It was also the only way to get alignment. A platform that tries to please everyone pleases no one.
The process change mattered as much as the architecture change. v1 was built reactively — someone requests a feature, a developer implements it, we discover edge cases in production. v2 inverted this.
Every feature starts as a spec. The spec defines the data model, the state machine, the API contract, the permission model, and the UI behavior. The spec gets reviewed by the people who will use the feature before anyone writes code. Arguments happen at the spec level, where they’re cheap, instead of at the code level, where they’re expensive.
We maintain 22 product specs and 14 design manifests. It sounds like a lot of documentation. It is. It’s also the reason v2 is shipping faster than v1 despite being a ground-up rewrite — the specs eliminate the ambiguity that made v1 development slow. When a developer (or an AI agent) picks up a task, the spec tells them exactly what to build. No guessing, no Slack threads, no “let me check with the product team.”
Flexibility is a feature you pay for every day. Every configuration option is a decision you’re deferring to the user — and users don’t want to make decisions about your platform’s internals. They want the platform to work.
The instinct to generalize early feels responsible. “What if a client needs X? What if the market shifts to Y? What if we need to support Z?” Building for those hypotheticals seems like good engineering. It’s not. It’s premature abstraction dressed up as foresight.
Build the opinionated thing. Build it for the workflow you understand today. When a real requirement contradicts your opinion, you’ll know — because a real customer will tell you, in specific terms, what doesn’t work. That’s a different conversation than “what if someone someday might need this to be configurable.”
We should have built the rigid thing first and loosened it when we had evidence. Instead we built the flexible thing first and are now rigidifying it at much greater cost.
At least we only had to learn this lesson four times.
Frank Thomas is CTO at Superkey Insurance and the founder of Koji.