The Catalog Said Yes, The Backend Said No
Our catalog advertised an integration our backend had never heard of. The customer saw an SSL error.
On a Sunday evening, a customer of ours sat down to connect her Freshservice account to her Righthand. She clicked the tile in our catalog, watched the OAuth window open, and got back a single, unhelpful sentence: an SSL error.
She tried again. Same error. She asked her IT lead at her company to take a look. He couldn't reproduce a working path either, so he did the thing every good customer does when a vendor's product is misbehaving: he sent us an email.
That email landed in Julius's inbox around 7 p.m. Pacific. By 7:15, it was in front of Avery, our PM. By 7:45, we had told the customer the wrong thing. And by the time we untangled what was actually happening, the bug had taught us something about the seams between a product catalog and a production backend that we think is worth writing down.
This is the story of how a "supported integration" wasn't, and what we are doing so it doesn't happen again.
The cold open: an SSL error that wasn't an SSL error
If you have shipped anything against third-party APIs, you have seen this shape of report before. A user tries to connect a service. Something fails in a way that the browser, the OAuth provider, or some intermediate proxy describes in language that sounds infrastructural. TLS. Certificate. Handshake. The error message is technically accurate in the same way that "your car will not start" is accurate when the problem is that there is no engine in it.
The customer's report read, paraphrased: "I'm trying to connect Freshservice and I keep getting an SSL error. Can you take a look?"
This is a perfectly reasonable thing to ask. From her side, the product had offered Freshservice as a thing she could connect, she had clicked the button, and the system had returned a low-level network error. Of course she thought it was SSL. We told her it was SSL too, because for the first hour, we were looking at the wrong thing.
The first investigation: looking under the wrong streetlight
Our first move was the obvious one: pull the production logs and search for SSL errors and Freshservice mentions in the relevant time window. We have decent log search. We knew the customer's account, we knew the approximate timestamp, and we knew the keyword.
The query came back clean. Zero SSL errors in the connector path. Zero log lines mentioning Freshservice. Nothing.
There is a particular flavor of debugging panic that sets in when you cannot reproduce a customer's stated symptom in your own logs. It tempts you toward one of two wrong conclusions. The first is that the customer is mistaken. The second is that something is broken so deeply that it isn't even logging.
We arrived at a third wrong conclusion, which was worse than either of those: we decided the customer must have been trying to connect a service we didn't support. Avery replied to Julius saying as much. Freshservice, we said, isn't a connection Righthand offers. The customer must be confused.
This is the kind of answer that feels rigorous (we looked in the logs, the logs were empty, therefore the request never reached us) and turns out to be the answer of a team that has not yet found the actual bug. We had drawn a conclusion from absence of evidence without checking whether the absence itself was the bug.
The screenshot
A few minutes later, Julius pushed back. Not with an argument, but with a screenshot.
The screenshot showed our own product. Specifically, it showed the catalog page where users browse available integrations. And there, in the grid, in our own UI, was a Freshservice tile. With our logo styling. Clickable. Live in production.
This is the moment in every debugging story where the model of the system you have been carrying in your head suddenly has to absorb a fact that doesn't fit. We had told a customer she couldn't connect a thing. Our own product had told her she could. Both statements had been issued, with full confidence, by the same company on the same day.
So we went looking for the actual bug.
The turn: catalog and backend disagree
Righthand's integration surface has two halves that need to agree with each other.
The first half is the catalog: the set of services we advertise to users on the browse page. This is what the user sees. It is rendered from a content source that defines the tiles, the logos, the descriptions, and the "connect" affordance.
The second half is the backend connections registry: the set of services our system actually knows how to talk to. This is what the user gets. It is queried at runtime through an internal API called listavailableconnections, and it is the source of truth for which OAuth flows, token schemas, and capability adapters have been wired up.
In a well-formed integration catalog, these two halves are derived from each other. Either the catalog is generated from the backend registry, or the backend registry is generated from the catalog, or they are both generated from a single shared schema. The point is that they cannot drift, because there is no two of them to drift between.
We did not have that. We had two sources of truth, and on this particular Sunday, they disagreed.
When Avery queried listavailableconnections directly, the response came back with a familiar-looking but subtly wrong list. The backend knew about Freshsales. It knew about Freshlearn. It knew about FreshBooks. It knew about Freshmarketer. It did not know about Freshservice.
The catalog had a Freshservice tile. The backend had no Freshservice integration. The customer had clicked a tile that pointed at a service the rest of our system had never heard of.
The root cause, in one paragraph
When the user clicked "connect," our frontend kicked off an OAuth handshake for "Freshservice." Somewhere downstream, the request hit a part of the system that expected to find a configured integration record for that identifier and didn't. The handshake failed in the kind of half-initialized state that tends to surface as a TLS or certificate error at the browser, because the redirect, callback, and signing material were never fully assembled. The "SSL error" the user saw was not an SSL problem. It was an integration that didn't exist, failing as loudly as it knew how, in the wrong vocabulary.
This is the headline of the whole story, and it is worth naming clearly: what the catalog advertised was not what the backend supported. Everything else, including the misleading error and our own wrong first answer, was downstream of that single mismatch.
Why we missed it the first time
In retrospect, the reason our first pass came up empty is obvious. We searched the logs for the symptom the user described ("SSL error") and for the service name ("Freshservice"). Neither was going to be there in any volume worth finding, because the request had failed before it produced the kind of structured log entry our search was tuned for. The bug lived in a gap, and gaps don't log themselves.
There's a general lesson buried in that. When a customer report doesn't match your telemetry, there are three possible worlds:
- The customer's description is imprecise, and the real event is in your logs under a different name.
- The event happened but isn't being logged.
- The event never reached the part of the system that logs, because it failed earlier than your instrumentation expects.
We jumped to world (1) and concluded the customer was confused. The actual answer was world (3). Failures that occur at the boundary between "what the product claims to do" and "what the product is wired to do" tend to live in world (3), and they are systematically invisible to log-driven debugging until you go looking for them at the configuration layer instead of the runtime layer.
The fastest way to find this class of bug is not better log search. It is to read the customer's report literally, then verify that every assertion the product made to that customer ("yes, you can connect this") is backed by a corresponding assertion the backend can make about itself ("yes, I know how to connect this"). When the two assertions don't match, you have found the bug, and you didn't need a log at all.
This is also why the screenshot mattered. Julius didn't bring new logs. He brought a fact about what the product was telling users, which we hadn't checked, because we had been busy checking what the product was telling itself.
The fix, short term and long term
The short-term fix was the boring one. We took Freshservice off the catalog page until the backend integration is actually built. We told the customer what had happened, in plain language, and apologized for the wrong first answer. Freshservice is a service we want to support, and now there is a real engineering ticket to build it rather than a tile pretending it already exists.
The longer-term fix is more interesting, and it is the part we want other teams building on third-party APIs to take from this post.
We are treating this as an instance of catalog-vs-capability drift, a category of bug we expect to keep meeting as our integration surface grows. The category looks like this: somewhere in your product, you make a public claim about what your system can do. Somewhere else in your product, the actual capability is configured. The two artifacts are edited by different people, at different times, through different tools, and there is no automated check that one matches the other. Over time, they drift. Users discover the drift before you do.
The defenses against this are not exotic. They are the same defenses you would apply to any other case of two sources of truth that need to agree.
- A single source of truth. Either generate the catalog from the backend connections registry, or generate the backend registry from the catalog, or generate both from a shared schema. We are moving in the first direction: the catalog should be a view of what the backend actually supports, not an independent document about what we wish it supported.
- Parity tests in CI. A test that pulls the current catalog, pulls
listavailableconnections, diffs the two sets, and fails the build if they disagree. This would have caught the Freshservice tile the day it was added, instead of the day a customer tried to use it. - Contract tests for the connect flow. For every integration we advertise, a smoke test that initiates the connect flow end-to-end (or at least up to the OAuth boundary) and confirms the handshake gets past the point where a missing integration record would fail. Real integrations stay green. Phantom integrations turn red immediately.
- Better error surfacing at the boundary. When a user clicks "connect" on an integration that isn't actually wired up in the backend, we should fail loudly and clearly, not with a TLS-flavored error that misleads everyone, including us. The error the user sees should be a true statement about the world: "This integration isn't available yet."
None of these are clever. All of them are the kind of thing teams skip in the first six months because the catalog and the backend started out in sync and it felt fine. They stop being optional the moment you have enough integrations that no single person can hold the full list in their head. That moment arrives earlier than you expect.
What this taught us about building Righthands
A Righthand is, among other things, a thing that connects to other systems on a person's behalf. The set of services it can reach is part of what it is. When we advertise a connection we can't actually make, we are not just shipping a broken button. We are misrepresenting what kind of collaborator the product can be. That matters more to us than the immediate bug.
It also taught us something we already half-knew, which is that the most useful person on an incident is often the one who brings a fact from outside the system you are searching. We were inside the logs. Julius was inside the product. The customer was inside the experience. Three vantage points, and only one of them had the screenshot that ended the wrong investigation and started the right one. Debugging at this layer is a team sport, and the team includes the customer.
We owe that customer, and her IT lead, a clearer answer than the one we gave on Sunday night. We owe future customers a product where the catalog and the backend cannot drift apart without us noticing first. Both are now on the roadmap, and both are the kind of work we'd rather do once, properly, than keep paying for in 7 p.m. emails.
If you are building on top of third-party APIs, the thing to take from this post is small and specific. Find the place in your product where you tell users what you support. Find the place in your system where that support is actually configured. Write the test that compares them. Run it tonight.
If those two lists don't match, your users will find out before you do. Ours did.