· Gradr Engineering

Scaling a Codebase (4x) Without Scaling the Team

Our previous posts have focused on internal dev-tooling, automation and compile-time guarantees, almost to the point of pretentiousness. We wrote 107 custom ESLint rules. We built a compile-time WCAG contrast checker. We gave every database column its own branded type.

The uncomfortable question: did it actually work?

The short version is: 4× LOC (lines of code) per person, in 6 months, with fewer people.

The LOC problem

As any rational developer is aware, LOC is in most regards a terrible metric for output. 10k lines can mean a micro-SaaS or generated boilerplate.

Measuring developer output is hard, really hard. But when every measurable metric moves in the same direction by a wide margin, the signal cuts through the noise. LOC is a bad metric — except when every metric agrees. Here's what happened between September 2025 and March 2026:

4.2× total LOC growth | ~4× LOC per person | 44 DB tables rewritten

  • Codebase: 51k → 219k LOC
  • LOC per person per month: ~1,900 → ~7,750
  • API routes: 148 (JS) → 187 (TS), all rewritten
  • DB tables: 75 → 95 (net: 44 deleted, 44 new = full domain model rewrite)
  • Active developers: 5 → 4

None of this is generated boilerplate.

So what actually shipped to users?

A subset of what we launched in 6 months:

  • Tenant-based audit logging (every DB read/write logged and tagged with semantic meaning)
  • Complete rewrite of all core features
  • Server-side worker infrastructure for autonomous jobs
  • An entire insight feature, with stateful server-side agents built in
  • Upgrade exam platform from demoable to national-exam grade robustness
  • Deprecation of an entire legacy microservice
  • Logical tenant isolation
  • Complete restructure of some of our most central data models And probably a lot of other stuff the author is forgetting

Each of these was scoped to one person over a couple of weeks. That's the real headline: not the LOC count, but that a single person could take on something like audit logging (which touched hundreds of call-sites) without bringing the codebase down.

The knee in the curve

LOC per person per month

The growth wasn't linear. For the first few months, the codebase crawled: a few thousand LOC per person, normal startup pace. Then something bent.

In November 2025, we hit 9,284 LOC per person. To put that in perspective: the entire first year of the codebase was ~39,000 LOC.

This wasn't a hiring spree. It was infrastructure compounding.

The knee in the curve maps onto infrastructure landing, but it wasn't a series of clean cutovers. It was gradual:

  • Jun 2025: TypeScript introduced (codebase was pure JS + Svelte)
  • Nov 2025: Every database column got its own branded type. That meant a generated type for every column (~500 files), plus the first ~40 custom lint rules to enforce them. All within the same week. Immature at first, gradually extended over the following months.
  • Dec 2025: Typed database client generated, so queries return branded types instead of raw Prisma output. Agents could now touch the database without humans reviewing every query.
  • Spring 2026: Lint rules grow from ~50 to 116.

Each piece unlocked the next: TypeScript → branded types → typed DB client → safe agent writes. None of it landed fully formed.

We didn't get faster by typing faster. We got faster by building reasons the code couldn't be wrong, so agents could write it without humans reviewing every line.


Here's the same API endpoint, before and after. Same logic, same database query:

// Before (2025): +server.js
import { prisma } from '$lib/server/prismaClient';
import { json200, json400 } from '$lib/server/commonResponses';
import { withRole } from '$lib/server/serverFunctions.js';
import { Role } from '$lib/clientConstants';

export const GET = withRole(Role.SALES, async () => {
	try {
		const academicLevels = await prisma.academicLevel.findMany({
			select: { id: true, name: true }
		});
		return json200(academicLevels);
	} catch (error) {
		console.error('Error getting academic levels:', error);
		return json400('An error occurred while getting academic levels');
	}
});
// After (2026): +server.ts
export const GET = api.rest(
	getAllAcademicLevelsContract.list,
	{
		authorization: { role: Db.UserRole.Role.SALES },
		audit: {
			target: 'collection',
			description: 'List academic levels',
			resourceType: 'academic_level',
			visibility: Db.AuditLog.VisibilityKind.Enum.visibility_internal
		}
	},
	async ({ locals }) => {
		const result = await dbSafe.academicLevel.findMany({
			select: { id: true, name: true }
		});
		if (!result.ok) return api.fail(result.error);
		return { status: 200 as const, body: result.value };
	}
);

The after is longer, sure. But the try/catch is gone, the error handling is typed (no more json400('An error occurred') swallowing the actual error), the contract enforces the request/response shape, the audit log is built in, and the role check is declarative. An agent can write the "after" version and the compiler will tell them if they got the contract wrong. An agent writing the "before" version could do almost anything and it would compile.

Bloopers

When researching for this post, we noticed the entire story is visible just by reading commit messages.

Pre-autumn-2025, our git log read like a confessional:

  • kinda cursed but works
  • nuked so much fucking shit code its insane
  • Garbage fucking shit
  • fixed cursed as shit
  • Fix sentry (hopefully god please)
  • SMOOTH AS FUCK

Post-infra (spring 2026), the same git log started reading like a changelog:

  • migrate id types
  • tighter type contracts
  • better type proofs
  • migrate more db cols
  • migrate boolean namespaces
  • less runtime validation

The shift isn't cosmetic. The early commits are emotional: relief, frustration, prayer. Someone is wrestling with a codebase they can't fully trust. The later commits are mechanical: what was done, to what. Someone is executing a plan. The lint rules and branded types came after the pain, not before. They're what turned the prayer into the plan.

The thesis

Scaling a codebase is hard. Scaling it without scaling headcount is harder. The conventional answer is "hire more people." We went the other way: people moved on, and we chose to invest in infrastructure over backfilling.

The bet paid off, but not because infrastructure made everyone 4× faster. It paid off because it made a small core's output survivable. 116 lint rules, a branded type system, and a typed DB boundary meant that agent-generated code couldn't drift. The guardrails are what let 4 people absorb 4× the output without the codebase turning to spaghetti.

We should be honest about what this is and isn't:

  • LOC is still a proxy. 168k lines includes generated code, type boilerplate, migration files. The real metric is the product surface: 187 API routes, 44 new DB tables, the feature list above.
  • The team didn't shrink on purpose. People moved on; we chose to invest in infra over backfilling. That's a strategy, not a layoff.

We're still not sure if this scales. But 4 people, 4× the output, and a codebase that didn't collapse is a better signal than anything we expected.

Share this post