Cross-posted from carrick.tools.
Every developer has looked at a survey widget, the little "How was your experience? 😞 😐 😊" popover, and thought: I could build that in a weekend. And you could. A form, a POST /responses, a GET /surveys, somewhere to look at the answers. Six endpoints, maybe eight if you're feeling enterprise about it.
Formbricks is that product, built properly, in the open, over several years. I recently indexed its entire system as part of a series of scans I've been running against open-source projects: the main app, its two SDKs and its n8n integration, four repositories in total, with every API endpoint and every outgoing call extracted straight from the source.
The survey is six endpoints. The product is 167.
This post is about where the other 161 come from. Not because they shouldn't exist. Spoiler: every single one of them earns its place. But the gap between "the thing" and "the product around the thing" is the most underestimated number in software, and for once we get to look at it directly.
The weekend version
Here is the part of Formbricks that matches the product on the landing page:
GET /api/v1/client/:workspaceId/environment # what surveys should I show?
POST /api/v1/client/:workspaceId/displays # I showed one
POST /api/v1/client/:workspaceId/responses # here's an answer
PUT /api/v1/client/:workspaceId/responses/:id # here's the rest of the answer
POST /api/v1/client/:workspaceId/user # who's answering
POST /api/v1/client/:workspaceId/storage # they uploaded a file
Six endpoints. Fetch the surveys, show one, collect the answer. If you squint, it's the weekend project.
Now let's leave the landing page.
Browsers happen
Those six endpoints get called from other people's websites, and browsers have rules about that. Before a browser lets a page on someone else's domain call your API, it sends a preflight request to check it's allowed. So nearly every client endpoint needs a matching OPTIONS handler just to say yes.
Then there's versioning. The Formbricks SDK is installed on other people's websites, and you can't force the whole internet to upgrade on your release schedule. So the client API exists twice, as v1 and v2, running side by side, indefinitely.
Six endpoints are now 27. Nothing happened. No feature was added. The survey got zero better. The count quadrupled because browsers and old SDK versions are the physics you operate in.
(A detail from the scan that shows versioning in real life: Formbricks' two SDKs currently call different versions of the same endpoint. The React Native SDK moved to v2, the JavaScript SDK is still on v1. Both work fine. That's what a migration looks like while it's happening.)
Scripts happen
The moment real teams use your product, someone wants to do everything the UI does from code. So: a management API. Surveys, responses, contacts, contact attributes, webhooks, file storage, all with create, read, update and delete, spread across v1 and v2 like everything else.
That's 46 endpoints. It's the least surprising section of the codebase, and it's also more than a quarter of the entire API, for a capability no screenshot will ever show.
Then there's a newer v3 API, 20 more endpoints, where you can see the product's future arriving: POST /api/v3/surveys/generate, survey templates, validation, a whole feedback-classification pipeline. The AI-era surface, growing alongside the two generations it hasn't replaced.
Big companies happen
Login in the weekend version is a session cookie. Login in the product is 29 endpoints, and every line on it is something a customer once refused to sign without:
GET|POST /api/auth/** # the auth framework's catch-all
GET /api/auth/saml/authorize # enterprise single sign-on
POST /api/auth/saml/callback
POST /api/auth/saml/token
GET /api/auth/saml/userinfo
GET /api/auth/sso/recovery/complete # for when SSO locks everyone out
GET /.well-known/openid-configuration/**
GET /.well-known/oauth-authorization-server/**
GET /.well-known/oauth-protected-resource/**
ALL /api/envoy-auth/** # 7 methods
ALL /api/traefik-auth/** # 7 methods
POST /api/mcp # your AI agent wants in too
SAML is the login system large companies insist on, and it takes four endpoints plus a recovery route for the day it locks everybody out.
My favourite two lines are envoy-auth and traefik-auth. People who self-host Formbricks put it behind a gateway (a server that sits in front of your app and filters traffic), and the two popular gateways each have their own way of asking "is this request allowed?" So the app answers that question twice, in two dialects, across fourteen endpoint-and-method combinations. That's not login for users. That's login for the infrastructure standing in front of the app.
And those .well-known addresses? They make the survey tool a full OAuth provider, the same kind of "Sign in with..." system Google or GitHub runs. That surface exists for AI. The metadata endpoint recognises exactly one resource, /api/mcp, the route where AI assistants connect, and the configuration lives in a file called mcp-oauth-provider-options.ts with self-service client registration switched on.
The MCP specification requires OAuth for HTTP servers, so none of this is a quirk of Formbricks — it is what conformance looks like, and it is spreading. Two years ago this layer did not exist for anybody. Now a survey tool runs its own login provider so that AI agents can register themselves and sign in to ask it questions. The fringe grew a fringe.
Other products happen
Nobody wants survey answers to live in the survey tool. They want them in Slack, Notion, Airtable and Google Sheets. Connecting to each of those means an OAuth handshake: you get sent to their consent screen, click Allow, and get redirected back. The app hosts 9 endpoints whose only job is to catch those redirects:
GET /api/v1/integrations/slack + /callback
GET /api/v1/integrations/notion + /callback
GET /api/v1/integrations/airtable + /callback + /tables
GET /api/google-sheet + /callback
And that's just the inbound half. The scan also captures every outgoing call the app makes, and the traffic flows the other way too: posting messages to Slack, creating pages in Notion, five different Airtable calls, Google token exchanges. The product isn't just an API. It's a customer of half a dozen other products' APIs, and each of those relationships is more code than the original survey form.
Add 4 webhook endpoints so other tools can subscribe to your events, the same way you subscribe to Slack's. (Pleasingly, the scan matched these against Formbricks' n8n integration, which was built in January 2024 and still lines up with the live API today.)
The business happens
The remaining 32 endpoints are the product becoming a company: organisations, teams, roles, member management, a Stripe webhook for billing, file storage, health checks, a CSV importer. There's even a GET /legacy-organization-settings/:id/** redirect, a small archaeological record of a data model that no longer exists but whose URLs still arrive.
And the product phones out
Here's the part the endpoint count doesn't even cover. The same scan lists everything the app calls, and self-hosted Formbricks talks to roughly a dozen outside services that have nothing to do with collecting survey answers:
- Two bot-detection services, Google's reCAPTCHA and Cloudflare's Turnstile, because abuse happens and customers disagree about whose checker they trust
- Unsplash, because surveys have background images and someone will want to search for one
- GitHub's releases API, because the app checks its own latest release to tell you you're out of date
- The Formbricks cloud, for licence checks and usage reporting on the enterprise edition
- An email and CRM platform, for the vendor's own lifecycle emails
Every arrow points at a reasonable decision. There is no villain in this codebase.
It's not just surveys
Maybe feedback tools are unusually complicated? They're not. I ran the same scan across Documenso, an open-source document-signing product, another one-sentence idea. Its system came out at 116 endpoints and 148 outgoing calls, including a 13-message protocol between its embeddable signing widget and the app, because "sign a PDF" eventually means "sign a PDF inside someone else's product, in an iframe, with events firing back to the host page".
The pattern generalises, and every developer already knows it from the inside. Your side project was 90% done, and the remaining 10% was login, billing, integrations, browser rules and gateway configs, forever. What the scans add is the actual ratio. In Formbricks, the thing on the landing page is about 16% of the API surface. The other 84% is the membrane between the product and the world: browsers, big companies, gateways, other products, abuse, money, and time, in the form of API versions that can never die because someone's SDK from 2024 still calls them.
So the next time someone asks how long the product will take, here's a number to reach for. The feature is 16% of it. The world is the rest.
A note on method
The numbers come from static analysis at pinned commits (Formbricks d1c3edf and its satellite repos; Documenso 3ff7f70), run with Carrick, the context layer for polyrepo TypeScript teams. For this post it parsed each repository's source and extracted endpoints, including wrapped handlers and re-exported route files, plus outgoing calls even when they're built through wrapper functions and environment variables, then matched callers to endpoints across repositories.
The raw counts contained three obvious extraction artifacts, which I excluded; 167 is the honest figure. The commits are pinned and the repositories are public, so every specific claim in this post can be checked against source, and I re-verified the ones I quote directly before publishing.
Endpoints are one part of what Carrick indexes. It also captures exported functions with their intent descriptions, the dependencies between services, and the real request and response types on every endpoint. Agents query that index over MCP to find what already exists across an organisation, code against real contracts instead of guessing at JSON shapes, and reuse functions instead of rewriting them. In CI it checks every pull request against the same index for contract drift, duplicate functions and version conflicts. Carrick is free during pre-release at carrick.tools, and the full extraction data behind these counts is available at david@carrick.tools.
Top comments (1)
This is a great illustration of something many developers experience but rarely quantify: the product itself is often the easy part. The long tail comes from everything surrounding it—browsers, backward compatibility, enterprise authentication, gateways, integrations, billing, and operational realities. One nuance I’d add is that endpoint count isn’t the same as architectural complexity. A well-factored API can legitimately have many small, focused endpoints while remaining easier to understand than a handful of overloaded ones. The interesting metric here is not “167 endpoints,” but how much of that surface exists because software has to coexist with other systems and previous versions of itself. The phrase “The feature is 16% of it. The world is the rest.” captures that perfectly. It explains why estimates based only on the visible feature almost always miss the real engineering effort.