Hur ett mindre team skrev 168k rader på 6 månader, och varför skyddsräcken vid kompileringstid (inte fler personer) var det som gjorde det hållbart.

Våra tidigare inlägg har handlat om interna utvecklarverktyg, automatisering och garantier vid kompileringstid, nästan till gränsen för det pretentiösa. Vi skrev 107 egna ESLint-regler. Vi byggde en kontrastkontroll för WCAG som körs vid kompileringstid. Vi gav varje databaskolumn en egen branded type.
Den obekväma frågan: fungerade det faktiskt?
Den korta versionen: 4× LOC (rader kod) per person, på 6 månader.
LOC-problemet
Som varje förnuftig utvecklare vet är LOC i de flesta avseenden ett uselt mått på output. 10k rader kan vara en mikro-SaaS eller genererad boilerplate.
Att mäta utvecklares output är svårt. Men när varje mätbart mått rör sig åt samma håll med bred marginal skär signalen igenom bruset. LOC är ett dåligt mått, utom när alla mått är överens. Så här såg det ut mellan september 2025 och mars 2026:
4,2× total LOC-tillväxt | ~4× LOC per person | 44 DB-tabeller omskrivna
- Kodbas: 51k → 219k LOC
- LOC per person och månad: ~1 900 → ~7 750
- API-routes: 148 (JS) → 187 (TS), samtliga omskrivna
- DB-tabeller: 75 → 95 (netto: 44 borttagna, 44 nya = hela domänmodellen omskriven)
- Aktiva utvecklare: 5 → 4
Inget av detta är genererad boilerplate.
Så vad levererades faktiskt till användarna?
Ett urval av det vi lanserade på 6 månader:
- Audit-loggning per tenant (varje läsning/skrivning i databasen loggas och taggas med semantisk betydelse)
- Fullständig omskrivning av alla kärnfunktioner
- Worker-infrastruktur på serversidan för autonoma jobb
- En hel insiktsfunktion, med inbyggda stateful agenter på serversidan
- Provplattformen uppgraderad från demobar till en robusthet i klass med nationella prov
- Avveckling av en hel legacy-mikrotjänst
- Logisk tenant-isolering
- Total omstrukturering av några av våra mest centrala datamodeller
- Och förmodligen en massa annat som skribenten har glömt
Var och en av dessa var avgränsad till en person under ett par veckor. Det är den verkliga nyheten: inte antalet rader, utan att en enda person kunde ta sig an något som audit-loggningen (som berörde hundratals anropsställen) utan att få kodbasen att rasa.
Knäet i kurvan
Tillväxten var inte linjär. Under de första månaderna kröp kodbasen fram: några tusen LOC per person, normalt startup-tempo. Sedan böjde sig något.
I november 2025 nådde vi 9 284 LOC per person. För att sätta det i perspektiv: hela kodbasens första år var ~39 000 LOC.
Det här var ingen rekryteringsvåg. Det var infrastruktur som gav ränta på ränta.
Knäet i kurvan sammanfaller med att infrastrukturen kom på plats, men det var ingen serie rena övergångar. Det skedde gradvis:
- Juni 2025: TypeScript infördes (kodbasen var ren JS + Svelte)
- November 2025: Varje databaskolumn fick sin egen branded type. Det innebar en genererad typ för varje kolumn (~500 filer), plus de första ~40 egna lint-reglerna för att upprätthålla dem. Allt inom samma vecka. Omoget till en början, gradvis utbyggt under de följande månaderna.
- December 2025: En typad databasklient genererades, så att databasfrågor returnerar branded types i stället för rå Prisma-output. Agenter kunde nu röra databasen utan att människor granskade varje fråga.
- Våren 2026: Lint-reglerna växer från ~50 till 116.
Varje del låste upp nästa: TypeScript → branded types → typad DB-klient → säkra skrivningar från agenter. Inget av det kom färdigt från start.
Vi blev inte snabbare av att skriva snabbare. Vi blev snabbare av att bygga in skäl till att koden inte kunde vara fel, så att agenter kunde skriva den utan att människor granskade varje rad.
Här är samma API-endpoint, före och efter. Samma logik, samma databasfråga:
// 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 };
}
);Efter-versionen är längre, visst. Men try/catch är borta, felhanteringen är typad (ingen json400('An error occurred') som sväljer det faktiska felet längre), kontraktet upprätthåller formen på request och response, audit-loggen är inbyggd och rollkontrollen är deklarativ. En agent kan skriva "efter"-versionen och kompilatorn säger till om kontraktet blev fel. En agent som skriver "före"-versionen kunde göra nästan vad som helst, och det skulle ändå kompilera.
Bloopers
När vi gjorde research inför det här inlägget märkte vi att hela historien syns bara genom att läsa commit-meddelandena.
Före hösten 2025 lät vår git-logg som en bikt:
kinda cursed but worksRegrade maybe... other stuff is brokenFix slot props (still hate how this works)Probably working, will verify after meetingFix sentry (hopefully god please)
Efter infrastrukturen (våren 2026) började samma git-logg låta som en changelog:
migrate id typestighter type contractsbetter type proofsmigrate more db colsmigrate boolean namespacesless runtime validation
Skiftet är inte kosmetiskt. De tidiga commitsen är känsloladdade: lättnad, frustration, bön. Någon brottas med en kodbas den inte riktigt kan lita på. De senare commitsen är mekaniska: vad som gjordes, och med vad. Någon genomför en plan. Lint-reglerna och branded types kom efter smärtan, inte före. Det var de som förvandlade bönen till en plan.
Tesen
Att skala en kodbas är svårt. Att skala den utan att öka antalet anställda är svårare. Det konventionella svaret är "anställ fler". Vi gick åt andra hållet: folk gick vidare, och vi valde att investera i infrastruktur i stället för att ersätta dem.
Satsningen lönade sig, men inte för att infrastrukturen gjorde alla 4× snabbare. Den lönade sig för att den gjorde en liten kärngrupps output hållbar. 116 lint-regler, ett system av branded types och en typad DB-gräns innebar att agentgenererad kod inte kunde glida iväg. Skyddsräckena är det som lät 4 personer hantera 4× så mycket output utan att kodbasen blev spagetti.
Vi bör vara ärliga med vad det här är och inte är:
- LOC är fortfarande en proxy. 168k rader inkluderar genererad kod, typ-boilerplate och migreringsfiler. Det verkliga måttet är produktytan: 187 API-routes, 44 nya DB-tabeller och funktionslistan ovan.
- Teamet krympte inte med avsikt. Folk gick vidare; vi valde att investera i infra i stället för att ersätta dem. Det är en strategi, inte en neddragning.
Vi vet fortfarande inte om det här skalar. Men 4 personer, 4× så mycket output och en kodbas som inte kollapsade är en bättre signal än något vi hade förväntat oss.

