A friend was over. Non-dev, but genuinely curious about what I’ve been building for the last year. So I did what any founder would do — I opened the terminal, spun up the marketing site, and then, feeling smug, typed api.freebo.ai/docs-v2 into the browser to show him the full API surface.
Not 401, not “please sign in.” Actually gone. The endpoint didn’t exist.
I tried /docs. 404. /openapi.json. 404. I hit /health and it came back 200, so the API itself was very much alive. The docs were just… erased in prod. And I remembered why — because six months ago I’d read something about OpenAPI specs being a “recon goldmine” for attackers, and I had written a very responsible little config block that hid the docs on any Railway environment. Past-me was thorough. Present-me looked stupid in front of my friend.
What the config actually did
The Fastify app has this near the bottom of server.ts:

Read that carefully. docsEnabled is true only when I’m not on Railway — meaning docs work on my laptop and nowhere else. And even that generosity has a caveat: staging can opt in with ENABLE_SWAGGER=true, but production is a hard no. The X-Robots-Tag: noindex, nofollow hook fires on any non-prod environment too, so even if a docs URL leaked, Google wouldn’t crawl it.
It’s a good policy. It’s the policy I’d give someone else if they asked. It’s also the reason I looked like an idiot with a locked-down laptop in front of a friend who just wanted to see something.
The fix, which is really an escape hatch
I didn’t want to invert the policy. Public OpenAPI specs on prod are a bad idea for a booking platform — they map every endpoint, every parameter, every enum value. Any attacker who wants to fuzz your API stops guessing and starts reading.
But “unavailable to me” was also wrong. So I added a second knob: HTTP basic auth. If SWAGGER_BASIC_AUTH is set (a user:password string), and ENABLE_SWAGGER=true, the docs come back — but every /docs* and /openapi.json request needs an Authorization: Basic header.

The reason it’s basic auth and not JWT is that JWT is what the docs document. If the docs themselves require a JWT to view, you have a chicken-and-egg problem: you can’t learn how to authenticate without first authenticating. Basic auth is stupid, orthogonal, and disappears from the workflow the moment I close the tab.
I flipped both env vars on the staging Railway service. /docs-v2 came back behind a browser password prompt. I typed my creds, Scalar loaded, and my friend got the walkthrough he’d asked for — on staging, not prod, but the shape is identical.
The lesson isn’t “docs should be public”
The lesson is that “secure by default” without a legitimate escape hatch is just “broken by default” for the operator. I had built one wall — noindex plus route removal — and no door. Doors have hinges and locks; that’s fine. Walls with no doors mean you get to yell through them and hope your friend understands.
Three things this reinforced:
One. Every hard-off flag in a codebase should have a documented on-ramp. If future-me can’t turn it on within 30 seconds of remembering it exists, present-me is going to be humiliated in the exact demo moment it matters.
Two. “Recon surface” is a real concern for OpenAPI specs, but it’s not solved by 404. It’s solved by not exposing operations you don’t want probed and by rate-limiting the ones you do. The 404 is theater — a determined attacker just fuzzes the routes they’d have found in the spec anyway.
Three. The best escape hatches are boring. Basic auth over TLS, one env var, no new service. Every fancier scheme (Cloudflare Access, JWT-gated docs, IP allowlists) I could have built here would have taken an afternoon and added a moving part I’d forget about. Basic auth took eight lines and a Railway variable.
The broader pattern
I keep finding this shape in Freebo. I over-secure something in month three because I’ve just read the OWASP top ten again, and then in month nine some real workflow — a demo, a support debug, an integration test — trips over the wall I built. The fix is almost never “make it less secure.” It’s “add a labeled door with a lock that fits my key.”
The docs are back, my friend was appropriately impressed by the Formance ledger diagram (that’s a post for another day), and I now have a small internal ritual: every time I add a “disable in production” flag, I write the “here’s how to turn it back on for me” note in the same commit. Comments are one line. Onramps are one env var. That’s the whole discipline.
Prod is still dark. Staging is behind a password I know. And the next time someone asks to see the API, I don’t have to explain that I locked myself out of it. Which is, I think, what a founder should be able to say.