How I Start With Any Inherited Codebase

The first week of any inherited codebase isn’t coding. It’s getting the lay of the land.

Whether you’re joining a new company, picking up a project from another team, or stepping into a codebase that’s been running without much attention — the process is the same. You have to understand what you’re working with before you can improve it. This post is about that process.

Step 1: Clone Everything, Read the History

My process is always the same: clone everything and start asking questions.

I use Claude Code for the initial orientation. It uses a lighter model for reading files and a more capable one for analysis, which makes it fast for scanning a codebase. My first ask is always the same: read the README. Then we go through the commit history — what’s the most recent activity? What branches are active? What was the last branch merged into main? This tells you a lot about the state of the project before you read a single line of application code.

Understand the Deployed State

From there I try to understand the deployment process. How did they deploy this code? I look for CI/CD configuration — GitHub Actions, deployment scripts, anything. Then I dive into the cloud account to find where the product actually lives. What URLs is it running at? What services is it using? How can I interact with it as a user? Understanding the deployed state gives you a baseline for what “working” looks like — and a target for what your local environment needs to match.

Step 2: Get It Running Locally

This is where the README usually splits from reality.

Getting a codebase running locally is rarely straightforward. Dependencies have drifted, environment variables are undocumented, services expect other services to be running. I fight through it — dependency installation, figuring out which version of Node or Python they’re actually using, tracking down missing config.

For an API, I consider it a success when I can hit the Swagger page — most API frameworks generate one now, and it’s the fastest way to confirm the service is alive and you can see what endpoints exist. For a frontend, success is getting it to load in a browser.

Then comes the real test: getting the frontend talking to the backend. This usually means figuring out how the .env files work — is there a .env.local? What URLs does the frontend expect? I strive to get basic functionality working end-to-end: login, a simple data fetch, anything that proves the pieces are connected on my machine.

I usually add a few things early:

  • Logging on the frontend — so I can see what API calls it’s making and what’s coming back
  • A simple health endpoint on the backend — just to confirm communication is happening

These are small investments that save hours of guessing later.

Step 3: Codify It in a Makefile

Once I’ve figured out how to run everything, I codify it. I create a Makefile at the root of my workspace. It might seem old-fashioned, but a Makefile gives you a cheap, fast CLI interface to start any service you need to run and test against. Every Makefile I write starts with a self-documenting help target:

##@ Core
.PHONY: help
help:  ## Display this help message.
	@echo "Usage:"
	@echo "  make [target]"
	@awk 'BEGIN {FS = ":.*?## "} \
  /^[a-zA-Z0-9_-]+:.*?## / { \
			printf "\033[36m  %-45s\033[0m %s\n", $$1, $$2 \
		} \
		/^##@/ { \
			printf "\n\033[1m%s\033[0m\n", substr($$0, 5) \
		}' $(MAKEFILE_LIST)

The ##@ comments create sections, and the ## comment on each target becomes its help message:

##@ Services
.PHONY: start-api
start-api: ## Start the FastAPI backend on port 8000
	@cd services/api && uvicorn main:app --reload

.PHONY: start-web
start-web: ## Start the Vue frontend on port 8080
	@cd services/web && npm run dev

Run make help and you get a clean, organized menu of everything you can do. It’s a small investment that pays off immediately — instead of digging through READMEs or guessing at startup commands, you have a single entry point for the entire platform.

When I finish an engagement, I break the master Makefile into smaller ones in each repo — make run-web, make run-api — and update the README with copy-pasteable commands. It’s a poor man’s CLI, but it’s efficient. Anyone who pulls the repo can get it running without asking me.

Step 4: Map the System

Once I have every service running locally with a few make commands, the real picture starts to emerge. I can trace data flows end-to-end, see what’s connected, and — more importantly — see what isn’t.

The next artifact I create is a document called SYSTEM_ARCHITECTURE.md. It’s somewhere between an artifact document and a formal system architecture — really just data traces and notes. It covers:

  • What services exist and which ones are active. When you’re dropped into a company, the most recently updated repo isn’t always the one running in production. Sometimes the service that hasn’t been touched in months is the one that matters most.
  • What’s required to run the product locally. Can I get away with the Makefile running services via CLI, or do I need Docker? What depends on what? What’s the minimum set of things I need running on my laptop to mimic the product?
  • Where the databases are. Is there a dev environment I can point at? A production database? What are the connection details?
  • Where the credentials live. This is a big one. I audit the codebase for secrets — API keys, database passwords, anything that shouldn’t be in git. I pull them out, note what they’re for, and store them properly. This file obviously never gets committed.
  • Service dependencies. What talks to what. What order things need to start in. What breaks if one service is down.

I write it down as I go, updating it every time I learn something new. It’s not polished — it’s a working document that captures the state of the system as I understand it at any given point.

Having that shared document makes conversations with the team easier. When someone asks “why can’t we just…” I can point to the architecture and walk through the constraints together instead of it being a debate about opinions.

Step 5: Trace the Data Flow

Once everything is running, I open the UI and start clicking through it with dev tools and logging cranked up.

Match the UI to the API

The goal is to map every user action to the endpoint it hits. I click a button, watch what request fires, check what comes back. At the same time, I do a route scan on the backend — get the full list of every endpoint that exists. Then I scan the frontend to see which endpoints are actually being called. The union of those two lists tells you a lot: which routes power the core product, which ones are just customer management (profile, settings), and which ones exist in the backend but aren’t called by anything at all.

Understand the Database

I need to understand how the tables work — what the schema looks like, how data relates, what indexes exist. I’m also looking for pagination. If they have it, how does it work? If they don’t, that’s usually an easy optimization that makes the product feel noticeably more responsive.

I’m a big believer in ORMs. If the codebase doesn’t have one, I’ll often ask if I can implement one — not to manage the database, but to have the schemas represented as code. I set it up so the ORM mirrors the existing tables without trying to own them. This gives you type safety and query building without the risk of the ORM making unexpected schema changes. You can still drop down to optimized queries when you need to.

I understand people like raw SQL, but I find it to be a security risk and a maintenance problem. Raw SQL scattered through a codebase leads to subtle bugs — I once lost two days debugging a test failure that turned out to be a query selecting the same column twice. An ORM would have caught that at the type level.

Trace the Auth

I open dev tools, log in, and watch where the tokens go. How does authentication work — Cognito, JWT, API key? Where is the user_id set, and how does the frontend get that information? I also dig into the codebase to find where auth middleware is implemented and which routes it protects.

This matters especially if there might be architectural changes down the road. It’s easy to accidentally break auth when you’re restructuring how services talk to each other. And on a practical note — make sure you’re actually added to whatever auth system they’re using so you can log in and test as a real user. Sounds obvious, but it’s easy to overlook when you’re deep in the code.

When the Picture Clicks

There’s usually a moment where you understand how the frontend, backend, and databases all work together — how a user action flows from a button click through the API to the database and back. That’s when you can start making changes with confidence. Not because you know everything, but because you have enough of the mental model to predict what will break when you touch something.

Step 6: Document As You Go

The biggest risk of inheriting a codebase isn’t bad code — it’s lost context. Why was this built this way? What was tried and failed? What looks like technical debt but is actually a critical workaround?

I write a lot of markdown. Not because I love documentation, but because I was that next person — rebuilding context that lived in someone’s head. I document so the person after me won’t have to.

├── SYSTEM_ARCHITECTURE.md     # System documentation
├── SESSION_NOTES.md           # AI context — where we left off
├── TODO.md                    # Higher-level goals and tasks

Session Notes

The session notes aren’t for the founders — they’re for the AI. When I start a new day with Claude, it needs to know where we left off, what was accomplished, and what the current goal is. Goals shift over time, and without a running log, you lose that thread.

I have Claude help me write these at the end of each day or every few days: what was the state when we started, what did we achieve, what’s the next step. It’s a breadcrumb trail that keeps the AI (and me) oriented.

The TODO

Separate from session notes, I maintain a larger TODO document that outlines the higher-level goals — what was asked of me and what needs to happen to get there. This is the document I reference when talking to the team, because what was asked of me and what I actually end up doing often differ. Discovery reveals things the original scope didn’t account for, and I don’t like to leave skeletons when I find them. If I’m in a module fixing a bug and the code around it can be restructured into something more maintainable, I do it. The result is that “what I actually did” is usually a much larger piece than the high-level asks — but the codebase is better for it long-term.

Discovery Never Really Ends

I want to be honest about this: there’s no clean line between “discovery phase” and “building phase.” Discovery is ongoing. You can’t fully map a system before you start touching it. There are always more skeletons in the closet — dependencies you didn’t realize existed, things that break when you change something seemingly unrelated. You’ll break things you didn’t mean to break while trying to fix the things you were asked to fix.

That’s normal. You can’t be afraid to break things, so long as you intend to fix them and leave the code better than you found it. But don’t break things just to break things — it depends on the engagement and what you’re asked to do.

There’s always a temptation to propose a full rewrite. Sometimes I’d love nothing more. But I think that’s the lazy path. More often than not, you can add and migrate. A rewrite feels clean and satisfying, but it throws away all the hard-won knowledge embedded in the existing code — the edge cases someone already hit, the workarounds that exist for real reasons. Iterating is harder, messier, and almost always the right call.


This is part of a series on taking startups from demo to product. Next up: the iterate-vs-rewrite decision and a technical deep dive on the fixes that matter most. Stay tuned.