The project has been delivered. The agency has invoiced. The client now has a repository, a deployed app, and a README that says "see docs/setup.md" — which doesn't exist.
This is when they call me.
I've taken over enough agency codebases to have a process. Not a checklist you run through once and declare victory, but a way of thinking about an unfamiliar system that's already in production. The goal for the first few days is not to fix anything. It's to understand what you actually have — which is almost always different from what was documented or promised.
The First Rule: Read Before You Touch Anything
The instinct, especially if the code is visibly bad, is to start cleaning. Don't.
Until you understand the system, you don't know which parts are load-bearing. I've seen codebases where a function named doStuffOld turned out to be the function that actually ran in production because someone had quietly edited a config file to point at it after the "new" version broke something. If I'd deleted it based on the name alone, the app would have failed silently.
The first 48 hours are for reading: the repository, the deployment setup, the issue tracker if there is one, and any communication history you can access. You're building a mental model before you touch a single file.
One thing I check in the first hours that's easy to overlook: observability. Does error tracking exist? Are there logs somewhere, and can you actually query them? Are there uptime alerts, and do they go to someone who can act on them? The practical question is simple: if the system goes down at 2am, who finds out, and how? Agency projects frequently have no answer to this. That's not a code quality problem — it's an operational risk that belongs at the top of your assessment.
Set Up the Local Environment — and Document Every Obstacle
Getting the project running locally is the first real test. Do it from scratch: fresh clone, follow the README exactly, and write down every step that's missing, wrong, or requires tribal knowledge.
The list of obstacles you collect here becomes part of your takeover assessment. It also tells you something about code quality in general — a project whose local setup requires six undocumented environment variables and a Postgres version that's three major releases behind has almost certainly accumulated similar debt everywhere else.
Common things I find:
-
.env.examplethat's either empty or contains production values from someone's machine - Node version not specified anywhere (and the app breaks silently on newer versions)
- Missing setup steps for third-party services — "you'll need a Stripe account" but no indication which keys go where, or how to configure webhooks for local development
- Database migrations that don't run cleanly, or no migrations at all — just a SQL dump from a specific point in time
If you can't get it running locally, that's the first item in your assessment. The client often doesn't know this is a problem because the agency handed over a deployed URL, not a functional development workflow.
Map the Architecture from the Code, Not the README
If there's architectural documentation, read it — then set it aside and look at the actual code. The two rarely match, and the delta between them is where problems hide.
What I'm trying to understand:
What does the data flow look like? Where does a request enter the system, what touches it, what gets written to the database. I trace a few representative paths — a user creation, an order, a webhook — manually through the code. This is tedious but irreplaceable.
What are the actual dependencies? Open package.json. Not the categories, the specific packages and their versions. Agency projects frequently have dependencies that are two major versions behind, packages that have been abandoned, or three different date-formatting libraries installed simultaneously because different developers had different preferences.
What connects to what externally? Third-party API calls, webhooks incoming and outgoing, cron jobs, queues. These are the integration points where failures are hardest to debug and where undocumented behavior lives. Map them before anything else.
Where is state kept? Database, Redis, local memory, cookies, environment variables used as runtime config. If there's state living somewhere it shouldn't — like feature flags hardcoded in environment variables that can't be changed without redeployment — that's worth flagging.
Security: Look Here First
Before I think about performance or clean code, I look for security problems. These are the issues where "we'll fix it later" means "we'll fix it after the breach."
slug="rescue-projects"
text="Inherited an agency codebase and not sure what you're actually dealing with? I do this assessment professionally — code audit, risk triage, and a clear path forward."
/>
The things I check immediately:
Secrets in the repository. Run a search for anything that looks like a key or token: grep -r "sk_live\|pk_live\|AIza\|AKIA" --include="*.js" --include="*.ts" . — and check the git history too, because secrets removed from HEAD are still in the history. If production secrets have ever been committed, that's a hard stop — rotate them before anything else.
No authentication middleware on protected routes. Agency code often handles auth at the individual endpoint level, which means it's one missed check away from a gap. Look for a consistent auth layer, not scattered if (!user) return 401 calls.
SQL injection surface. If the project uses raw queries rather than an ORM, search for string interpolation in query construction. Even ORMs can be misused — look for db.raw() calls with user-supplied data.
Missing rate limiting. Login endpoints, password resets, API routes that do expensive operations — check whether any rate limiting exists. If it doesn't, add it to the assessment as a risk, not as a task for this week.
CORS configuration. Access-Control-Allow-Origin: * on an API that handles authenticated requests is worth flagging even if it's not actively exploitable today.
Dependency vulnerabilities. Run npm audit (or the equivalent for your package manager) and look at what comes back. Agency projects often arrive with dependencies that haven't been reviewed in a while. Critical or high-severity CVEs in production dependencies belong in the immediate-action tier, not the backlog.
I don't fix all of these immediately. I triage: what needs to happen today, what needs to happen this week, what goes in the medium-term backlog.
Tests: Do They Exist, and Do They Actually Pass?
Run the test suite if there is one. Then read the tests.
Agency projects frequently have tests that were written to satisfy a checklist, not to catch bugs. The signs: tests that only assert that a function returns without throwing; tests that mock every dependency to the point where they can't catch any integration issue; coverage numbers that look impressive because the same happy-path code is tested fifteen different ways while error handling is untested entirely.
Sometimes there are no tests at all. This is actually easier to work with than the false confidence of a suite that passes but doesn't cover anything meaningful.
What the test suite tells you: where the previous developers thought the risky parts were. Even bad tests are a form of documentation about intent.
Whatever you inherit, don't start by rewriting the tests. Start with characterization tests — tests that document what the code currently does, not what it should do. Run a critical path, capture the output, write a test that asserts it. This gives you a safety net before you touch anything, and it forces you to actually understand the behavior rather than assume it. Once you have that baseline, you can add meaningful coverage for the paths that matter most to the business.
Identifying Real Debt vs. the Appearance of Debt
Not all messy code is actually broken. This distinction matters because it affects where you spend time.
Appearance of debt — things that look bad but work fine:
- Inconsistent naming conventions across files (one developer used camelCase, another preferred snake_case in the same object)
- Functions that are longer than you'd write them but do exactly one clear thing
- Comments that are out of date but harmless
- Code that could be DRY but is repeated in a way that's easy to follow
Real debt — things that will cause failures:
- Error handling that consists entirely of
catch (e) {}— exceptions swallowed silently - No retry logic on external API calls that will occasionally fail
- Copy-pasted route handlers where a bug fixed in one was never fixed in the others
- Database queries inside loops (N+1 problems that are fine at current scale and catastrophic at 10x)
- No database migrations — state is only defined by the current schema, with no record of how it got there
The practical test: does this cause a bug today, will it cause a bug under more load, or is it just aesthetically unpleasant? Prioritize the first two. Don't spend the first sprint making things prettier.
Common Agency Code Smells Worth Documenting
These aren't dealbreakers, but they're worth naming in your assessment so the client understands what they inherited:
No error handling on async operations. A fetch call or database query that throws will surface as an unhelpful 500 with no context. In Node.js this can silently swallow errors in some contexts.
Copy-pasted route handlers. Instead of a shared function, the same 40 lines of logic duplicated across three routes with minor variations. Bugs fixed in one are silently present in the others.
Hardcoded config values. Database connection strings, API base URLs, or timeouts hardcoded in the middle of a function rather than environment variables or config files. Finding and changing them later is a manual search.
No database migration history. The database schema exists, but there's no record of changes over time. You can't safely run a fresh setup or roll back. Schema and application code are permanently coupled in an opaque way. This is one of the patterns I cover in detail in PostgreSQL production patterns — schema drift deserves its own section.
Deployment that only works from one person's machine. The CI/CD pipeline exists but requires manually uploaded files, environment variables set through a web UI with no documentation, or build steps that assume a specific OS path structure.
Write the Takeover Assessment
Before you start changing anything, produce a written document. This protects you and clarifies your thinking.
The assessment I write covers:
- What I found — architecture overview based on what's actually in the code, not what was described
- What runs locally and what doesn't — including every undocumented step I had to figure out
- Security issues — triaged by severity: immediate action, this sprint, backlog
- Actual vs. claimed functionality — if the spec said feature X was implemented and it isn't, or it's implemented partially, name it explicitly
- Risks — technical debt items that are not bugs today but will become bugs under specific conditions. For each one, frame it as probability × impact: "this will fail when concurrent users exceed X, and when it does, it takes the entire checkout flow down" is something a client can act on. "this code is not ideal" is not.
- Recommended path forward — stabilize first, then improve
The audience for this document is the client, not a developer. They need to understand what they're working with in terms of business risk, not code quality scores.
Stabilize First. Almost Never Rewrite.
The advice I give every time, even when the code is genuinely bad: don't rewrite from scratch. Before you commit to a rewrite at all, the right first step is a structured technical due diligence engagement — not jumping straight to rescue-project mode.
I understand the impulse. The codebase is a mess, it's faster to start clean, you won't carry forward the old decisions. Every developer who's ever inherited a bad codebase has felt this. It is almost always the wrong call.
Rewrites fail for a consistent set of reasons. The old code, however ugly, contains implicit knowledge about edge cases — validation rules, data inconsistencies in the database, integrations that behave unexpectedly under specific conditions. That knowledge took years and customer incidents to accumulate. A rewrite starts without it and rediscovers all of it the hard way. Meanwhile, the business is running on the old system and nothing new has shipped.
The better path: stabilize what exists, fix the actual security issues and bugs, and then incrementally improve. Replace the most problematic modules one at a time, with tests before and after — the strangler fig pattern works well here: build the replacement alongside the old code, route traffic to it gradually, then delete the original once you're confident. This is slower and less satisfying but it produces working software rather than a new codebase that will need its own takeover in two years.
The exception is when the architecture is fundamentally incompatible with the business requirements — not messy, but structurally broken in a way that makes adding new features nearly impossible without touching everything. That's rare. More often, "this needs a rewrite" is an aesthetic judgment dressed up as an engineering one.
When You've Done This
You'll have: a running local environment with setup fully documented, an architecture map based on actual code, a triaged security issue list, an honest assessment of what works and what doesn't, and a clear recommendation for next steps.
This takes three to five days for a medium-sized codebase. It's not glamorous work. But it's the difference between taking over a project and being taken over by it.
If you've inherited an agency codebase and aren't sure what you're actually dealing with, or if you need someone to do this assessment and tell you honestly what the path forward looks like — get in touch. I've done this across multiple production systems, including pikkuna.fi, and I know what to look for.
Top comments (0)