All insightsENGINEERING · Architecture · 4 MIN READ

How this site is built: four failures and one bypassed defence

A walkthrough of building roninsystems.dev, from prototype to a self-contained Next.js and FastAPI application. What broke along the way turned out to be more interesting than the stack.

Ronin Systems lived in several places at once: Telegram, YouTube, Telegraph, trading platforms, freelance profiles. The site had to gather that into one system and separate the two directions — Engineering and Systematic Trading.

It started as a visual prototype, which validated the concept quickly. After that the technical foundation was rebuilt so the site would not depend on a site builder and could evolve like any other application.

What follows is not a list of technologies but an account of what broke. That is the more useful part.

What it runs on

Next.js 16, React 19, TypeScript. A separate FastAPI service on Python 3.12 for the contact form, PostgreSQL on Neon, Docker, and deployment to Fly.io — two apps in Frankfurt at 256 MB each. Articles and case studies in MDX with no CMS, two locales with an automated parity check, three colour themes applied before first paint.

Every page is prerendered and client-side JavaScript is kept small.

Bypassing the rate limit

The contact form is capped by requests per address. The check looked like this: seven requests in a row, each declaring its own X-Forwarded-For. The limit is five. All seven went through.

The reasoning is obvious — that header is trivially forged, so it cannot be trusted. We corrected the trusted-address source in the application, restarted, and repeated the test. Seven out of seven again.

The cause sat one layer up. Uvicorn enables proxy-header handling by default and trusts X-Forwarded-For from any peer, rewriting request.client at the ASGI layer before application code runs. The application-level fix was correct and completely pointless: it was bypassed before it could take effect.

The remedy is two things: --no-proxy-headers at startup, and a trusted header set by the site's own server-side proxy — the only route through which the API can be reached at all. Plus a test asserting the flag stays in the Dockerfile so it cannot be removed by accident.

The general lesson outruns the framework: before fixing how you trust a piece of data, find out who supplies it. A layer you did not think about may already have decided for you.

Three smaller failures

The site fell over at 256 MB. A request for a 1920px image variant killed the process through the OOM killer and took the health check with it. The problem was not machine size: the source image was 2816×1536 while the markup declared it as 1408×768 — four times larger than anything displayed. Sharp decodes an image to raw pixels before encoding it. The fix is to bring the source down to its declared size and cap the widths that may be generated at all.

Navigation landed mid-page. scroll-behavior: smooth on html applies not only to anchors but to the router's scroll-to-top on navigation, and the incoming page interrupts that animation. Confirmed by comparison on a single page: with smooth, three navigations from y=2400 ended at 2400, 2400 and 1970; with auto, all three ended at 0. Smooth scrolling came back scoped to a click on the table of contents.

The language switch led to a 404. English is served from the root and rewritten onto /en internally, so the router sees /en/engineering while the address bar shows /engineering. The link to the other locale was derived from the address and produced /ru/en/engineering. The fix is to read the path from the route tree rather than the address bar.

All three share a shape: each component behaved correctly in isolation and failed at a seam — application and runtime, CSS and router, router and proxy.

Two stylesheets stacked on each other

The original stylesheet ran to 1075 lines in which the second half redefined the first: :root declared twice, --max-width set to one value and then another, duplicated media queries, 201 font declarations and around thirty distinct clamp curves for first- and second-level headings alone.

The result read as a collection of separate elements rather than one page, because every block was positioned by hand — there was no shared grid to sit on.

It was rebuilt around a token layer: a type scale, spacing, twelve columns. No rule sets a font size directly. Colours and the visual language did not change.

What we deliberately did not build

No Redis. The only in-memory state is the rate-limit counter, and with one machine running it is correct. It becomes wrong under horizontal scaling — at which point one class changes behind the same interface.

No CMS. Articles and cases live in the repository as MDX. Content passes the same checks as code: the script fails if a document is missing its second locale or the heading structures have diverged.

No analytics. There is none — no counters, no tracking cookies.

What follows from this

A company website is usually filed under marketing rather than engineering. Hence the site builders, the absence of tests, and the impossibility of verifying anything.

But it has exactly the properties of any other system: boundaries, state, trust in external input, behaviour under load. And it fails the same way — at the seams, not inside components.

The difference between shipping a website and building a system does not show on launch day. It shows six months later, when something has to be added to it.

Related reading