Adhiraj Singh ← All writing

We Froze the API Contract on Day One and Built the Whole Thing in Parallel

TerraSight is a full-stack system: a Python perception backend that turns rover camera feeds into a terrain map, and a Next.js + Three.js dashboard that renders it as a live 3D mission view. Two teams, a hackathon clock, and the usual way that ends — one side blocked on the other, a frantic integration night, shapes that don't line up.

It didn't end that way. The frontend worked with the real backend the first time they were connected. Here's the handful of decisions that bought that.

Freeze the contract before anything is real

On day one, before a single perception stage existed, I fixed the API — five endpoints, exact shapes — and locked them with a test:

EndpointReturns
GET /map/tiles[{ x, y, z, class, slope, safety_score, zone }]
GET /rover/path[{ t, x, y, heading, mode }]
GET /sites[{ id, x, y, safety_score, rank }]
GET /boundaries[{ type, polyline }]

Then I served those shapes from mock JSON — hand-written files in the exact contract — long before the pipeline that would eventually produce them. The frontend team built their entire 3D dashboard against the mock. The backend team built the real pipeline to fill the same shapes. Neither waited on the other, and neither could quietly drift, because a CI test checks the live backend's output against the frozen shapes on every push.

The trade is real and I'd make it again: I gave up the freedom to change those shapes casually later. Any change is now a coordinated event between two teams. That discipline is the feature — it's what makes the parallelism safe.

Make the database optional

The API serves live data from Supabase when it's configured, and falls back to the same mock JSON when it isn't — one code path, chosen by one environment variable:

db.fetch(...) → Supabase if SUPABASE_URL/KEY set, else mock/*.json

This sounds like a small convenience. It's actually what keeps the whole thing unblocked. The frontend never waits on database provisioning. The production deploy works with zero secrets — unconfigured, it just serves the mock, so a demo can't die on a missing credential. Swapping to live data is one env var, no code change. The only cost is a second source of truth (the mock) to keep consistent with the schema — cheap insurance against every "the demo is down because the database isn't ready" disaster.

Build the skeleton end-to-end, with honest stand-ins

The tempting hackathon plan is to perfect one thing — train the best segmentation model you can — and integrate at the end. That's how you arrive at the demo with a great model and no system around it.

I did the opposite. The first deliverable was a thin thread through every stage — segmentation → depth → SLAM → terrain → scoring → API → served — with each perception stage implemented as an honest classical stand-in behind a frozen internal contract (SegCell, DepthCell, FusedCell). Not stubs that return zeros. Real classical implementations that actually work, just less accurate than a trained model would be.

Two things fall out of that:

The bet was explicit: a complete system with a provable safety property and a working demo beats one half-trained model with nothing around it. In a time-boxed build, that's almost always the right bet.

Keep the deployed backend lean

One more that paid off: the deployed API installs requirements.txt only — no numpy, no OpenCV, no torch. The heavy CV stack lives in a separate requirements-cv.txt and never gets imported by the serving function.

The reasoning is a clean split: the serverless API only serves precomputed terrain products; the actual computer vision runs on the rover or offline. Loading a CV stack into the API would bloat cold starts and cost for zero benefit. So the deployed function stays small, fast, and cheap, and "serve" and "compute" never get tangled into one fat backend that does both badly.

The lesson

None of these are clever. A frozen contract, a mock fallback, an end-to-end skeleton, a lean deploy — they're boring, and boring is exactly why they worked. Each one removes a dependency between two people or two systems that would otherwise have to move in lockstep.

The hard part of shipping full-stack under a deadline was never the algorithms. It was the coupling — the frontend that can't start until the backend is real, the deploy that can't run until the database exists, the model that has to be finished before anything integrates. Every decision here is the same move: find the thing two teams are blocked on each other for, and put a frozen, honest stand-in in the middle so both sides can run flat out. Do that everywhere, and "integration night" is just the moment you flip an env var.