Smazání 1,4 milionu řádků
Právě jsem dokončil kompletní přepis backendu Kvalty.cz. Vyměnil jsem Directus za vlastní řešení. Takhle to doopravdy vypadalo.
Čísla mluví sama za sebe:
- -1 392 860 smazaných řádků
- +334 224 nových řádků
- 181 commitů
- 3 týdny práce
Právě jsem dokončil kompletní přepis backendu pro Kvalty.cz. Vyhodil jsem Directus a nahradil ho vlastním řešením.
Proč musel Directus pryč
Nebudu lhát — Directus splnil svůj účel v začátcích. Když jsem byl Android vývojář tápající ve svém prvním webovém backendu, mít headless CMS, který mi dal GraphQL API out of the box, působilo jako dar. Ale dary mají skryté náklady.
Komplexita GraphQL mě požírala zaživa. Každý dotaz musel být ručně vytvořený s vnořenými fragmenty. Stránkování vyžadovalo relay-style kurzory, které se jemně rozbíjely. A pokaždé, když jsem potřeboval joinovat data přes 3+ tabulky — což se 163 tabulkami bylo neustále — skončil jsem s obludnými dotazy, které se nedaly debugovat. Jedna detailní stránka autoškoly vyžadovala 7 vnořených GraphQL fragmentů jen k načtení všech souvisejících kurzů, cen, recenzí a metadat.
Systém rozšíření byl křehký. Měl jsem 21 vlastních Directus extensions — hooky, endpointy, custom rozhraní. Pokaždé, když Directus vydal minor aktualizaci, minimálně 3-4 z nich se rozbily. API rozšíření nebylo stabilní. Trávil jsem víc času údržbou kompatibilních shimů než stavěním skutečných funkcí. Jeden památný víkend změnil Directus update způsob předávání hook context objektů a 9 z mých 21 extensions tiše přestalo fungovat v produkci. Dva dny jsem si toho nevšiml.
Výkon degradoval s růstem datasetu. S 1 400+ autoškolami, 15 000+ kurzy a všemi přidruženými ceníky, recenzemi a geografickými daty začala obecná dotazovací vrstva Directus ukazovat svůj věk. Složitá filtrovaná vyhledávání — jádro funkcionality Kvalty — trvala 800ms+ v dobrý den. GraphQL vrstva přidávala overhead, který přímý databázový dotaz nepotřeboval.
Žádná end-to-end typová bezpečnost. Tohle bylo rozhodující. Stavím TypeScript monorepo. Chci změnit sloupec v databázi a mít kompilátor, který na mě řve všude, kde je ten sloupec referencovaný — od API handleru po React komponentu. Directus mi dával auto-generované GraphQL typy, ale byly volné, často špatné po změnách schématu, a vždy existovala mezera mezi tím, co databáze doopravdy obsahovala, a tím, co si TypeScript myslel, že obsahuje. Debugoval jsem typové neshody za běhu v produkci. To není inženýrství, to je hazard.
Nový stack
Náhradní architektura je čistá a účelová:
Hono jako API server. Lehký, připravený na edge, běží na Cloud Run. Celý router se vším middleware váží míň než jedna Directus extension.
tRPC pro typově bezpečné procedury. Tohle je ten skutečný win. Definuju proceduru na serveru a klient přesně ví, co přijímá a vrací. Žádný codegen, žádné GraphQL schéma soubory, žádné doufání, že typy sedí. Změním návratový typ a každý konzument se rozbije při kompilaci. Přesně to, co jsem chtěl.
Drizzle ORM pro správu schématu a dotazy. Mých 163 tabulek je teď definováno v TypeScript. Migrace se generují automaticky z diffů schématu. Query builder mi dává plnou kontrolu — joiny, poddotazy, agregace — bez abstrakční daně, kterou Directus vybíral. Ty 800ms vyhledávání? Dolů na 90-120ms.
Better Auth nahrazující zabudovaný auth systém Directus. OAuth flow, správa sessions, přístup na základě rolí — všechno obsluhuje dedikovaná knihovna místo toho, aby to bylo propletené s CMS. Oddělení auth od správy obsahu byl upgrade zdravého rozumu, o kterém jsem nevěděl, že ho potřebuju.
Strategie migrace
Nemůžete prostě vypnout produkční systém obsluhující tisíce uživatelů a doufat, že nový bude fungovat. Takhle jsme to udělali.
Týden 1: Mapování schématu a paralelní infrastruktura. Namapoval jsem každou Directus kolekci a pole na její Drizzle ORM ekvivalent. Všech 163 tabulek, jejich relace, jejich omezení. Claude a já jsme je procházeli systematicky — 20-30 tabulek za session. Mezitím jsem nastavil nový Hono/tRPC server vedle existující Directus instance. Oba běžely, oba sahaly na stejnou PostgreSQL databázi. Nový stack mohl číst data, ale ještě neobsluhoval žádný provoz.
Týden 2: Migrace endpointů a datové skripty. Tohle byla ta brutální část. Každý API endpoint, který tři Next.js aplikace konzumovaly — a bylo jich přes 80 odlišných datových vzorů — musel být reimplementován jako tRPC procedura. Napsal jsem migrační skripty pro data, která Directus ukládal ve svém vlastním interním formátu (uživatelské sessions, metadata souborů, historie revizí). Některá data se přesouvala jednoduše. Některá byla uložena v Directus-specifických JSON blobech, které potřebovaly parsování, transformaci a opětovné vložení. Spouštěl jsem paralelní validační skripty, které porovnávaly odpovědi ze starého a nového API pro každý endpoint. Pokud se odpovědi neshodovaly byte po bytu (po normalizaci), migrační skript to označil.
Týden 3: Přepnutí, migrace auth a úklid. Přepnul jsem DNS. Všechny tři Next.js aplikace mířily na nové tRPC endpointy. Better Auth převzal správu sessions. 21 Directus extensions bylo smazáno. Závislost na Directus — a jeho 1,39 milionu řádků node_modules — zmizela z lockfilu. Tohle byl týden, kdy přistálo nejvíc z těch 181 commitů, protože v produkci najednou vypluly všechny edge cases.
Když se AI splete
Nebylo to „na první pokus, perfektní výsledek”. Několikrát jsme se s AI vážně nepochopili a některé složitější části potřebovaly iteraci, aby v produkci skutečně fungovaly. Žádný magický autopilot se nekonal.
Incident s PostGIS geography. Požádal jsem Claude o migraci geografických vyhledávacích dotazů z vlastní filtrační syntaxe Directus na raw Drizzle dotazy s PostGIS. Claude předpokládal, že používám typy geometry. Já používal typy geography. Rozdíl je zásadní — geometry dělá planární matematiku, geography sférickou. Každý výpočet vzdálenosti se lišil o 15-30 % v závislosti na zeměpisné šířce. Školy blízko okrajů České republiky byly vyloučeny z rádiusových vyhledávání, která je měla zahrnout. Trvalo mi dva dny, než jsem si toho všiml, protože výsledky vypadaly „dostatečně blízko” v Praze, kde žila většina mých testovacích dat. Poučení: vždycky testujte edge cases na geografických okrajích, ne jen ve středu.
Předpoklad formátu auth sessions. Claude vygeneroval integraci Better Auth s předpokladem, že sessions budou uloženy jako JWT v cookies. Moje existující nastavení Directus používalo neprůhledné session tokeny se server-side úložištěm. Během migrace Claude postavil JWT-based flow, který v dev prostředí fungoval perfektně — ale v produkci měli existující přihlášení uživatelé neprůhledné tokeny, které nový systém nerozpoznal. Každý aktivní uživatel byl tiše odhlášen. Ne katastrofa, ale ne ideální pro 200+ uživatelů, kteří měli aktivní sessions to sobotní ráno.
Transakční zámek při batch insertu. Během datové migrace Claude napsal skript, který vkládal záznamy kurzů v jedné masivní transakci — 15 000 řádků. Na mém lokálním Postgresu v pohodě. Na produkční Cloud SQL instanci se souběžnými čteními to zamklo tabulku kurzů na 12 sekund. Tři Next.js aplikace vyhodily 504 timeouty. Musel jsem transakci zabít, přepsat ji na dávky po 500 se SAVEPOINT markery a spustit ji v okně nízkého provozu ve 3 ráno.
Dynamika člověk-AI
Fungovalo to jako extrémně intenzivní pair programming. Já hrál architekta, nastavoval mantinely a Claude dělal surový těžký lifting. Musel jsem dělat poctivé code review, stavět se na odpor a držet směr, když začal přicházet s nesmysly.
Typická session vypadala takhle: nastínil jsem 5-8 tRPC procedur, které potřebovaly existovat, popsal očekávané vstupy a výstupy, specifikoval, které Drizzle dotazy použít, a nechal Claude je vygenerovat. Pak jsem recenzoval každou proceduru — kontroloval správnost dotazů, error handling, zúžení typů a edge cases. Asi 60 % vygenerovaného kódu šlo do produkce tak, jak bylo. 30 % potřebovalo drobné korekce (špatný název sloupce, chybějící null check, neefektivní join). 10 % potřebovalo zásadní přepracování nebo úplně jiný přístup.
Těch 60 %, co šlo rovnou, je důvod, proč to trvalo 3 týdny místo 6 měsíců. Těch 10 %, co potřebovalo přepracování, je důvod, proč nemůžete nechat AI běžet bez dozoru.
Po migraci: Co se rozbilo v produkci
I s paralelní validací produkce vždycky najde to, co testování nenajde.
Nekonzistence řazení. Directus měl výchozí řazení pro kolekce, které jsem nikdy explicitně nenastavil — řadil podle interního row ID. Nové Drizzle dotazy řadily podle created_at. Pro většinu dat bylo pořadí stejné. Pro 47 autoškol, které byly hromadně importovány ve stejný den s identickými timestampy, bylo pořadí náhodné. Tři uživatelé nahlásili, že „se výsledky pořád mění.” Oprava: explicitní deterministické řazení se sekundárním řadícím klíčem.
Změna formátu URL souborů. Directus servoval soubory přes vlastní asset pipeline (/assets/{uuid}). Nový systém je servoval přímo z Cloud Storage. Aktualizoval jsem URL v databázi, ale přehlédl 340 polí s popisem kurzů, které měly Directus asset URL natvrdo v rich text HTML. Oprava: migrační skript s regex nahrazením plus redirect pravidlo na staré asset cesty jako záchranná síť.
Mezera v rate limitingu. Directus měl zabudovaný rate limiting, na který jsem zapomněl. Nový Hono server se spustil bez jakéhokoliv. Po dobu 6 hodin v úterý bylo API úplně nechráněné. Nic špatného se nestalo — ale mohlo. Oprava: přidání hono-rate-limiter middleware do hodiny od zjištění.
Výsledek
Stálo to za ty nervy. Backend je teď výrazně kvalitnější, kód je čistý a hlavně — teď se s ním pracuje jako radost.
Pár konkrétních čísel před/po:
| Metrika | Directus (Předtím) | Vlastní stack (Potom) |
|---|---|---|
| Studený start | ~4,2s | ~1,1s |
| Filtrované vyhledávání škol | 800-1200ms | 90-120ms |
| Kompletní cyklus nasazení | ~8 min | ~3 min |
| Typová bezpečnost end-to-end | Ne | Ano |
| Závislosti (lockfile) | 1 847 balíčků | 412 balíčků |
| Vlastní extensions k údržbě | 21 | 0 |
Developer experience je metrika, která se nevejde do tabulky. Předtím: děs. Každá funkce znamenala boj s názory Directus na to, jak by data měla proudit. Potom: přemýšlím o funkci, popíšu ji a postavím ji. Framework ustoupí z cesty.
Claude Code právě měl narozeniny — jeden rok — a za tu dobu jsme s ním ušli obrovský kus cesty. Celé Kvalty jsem vytvořil s ním — projekt, který je už teď obrovský a roste, ale ve skutečnosti jsme pořád na začátku.
Tento přepis byl zatím největší výzva, kterou bych rozhodně nezvládl bez toho roku zkušeností a vzájemné kalibrace.
Co to celé umožnilo
AI je neuvěřitelně mocný nástroj. Ale musíte vědět, jak s ním pracovat.
Neexistuje žádná zkratka kolem porozumění vlastnímu systému. Přepis fungoval, protože jsem věděl, co starý backend dělal špatně, co nový potřeboval dělat správně, a mohl jsem ověřit každé rozhodnutí, které Claude udělal. Bez toho kontextu by stejný přepis byl katastrofa.
181 commitů. 3 týdny. 1,4 milionu řádků pryč. A systém je na tom líp.
Zvládl bych to bez AI? Technicky ano — za možná 6 měsíců, s vyhoření a pravděpodobně pár výpadky produkce. Zvládlo by to AI beze mě? Rozhodně ne. Nevědělo, proč musí Directus pryč. Nevědělo, které ze 163 tabulek jsou důležité a které jsou interní Directus balast. Nevědělo, že rozdíl geography/geometry nás kousne v pohraničních regionech Česka.
Přepis fungoval, protože obě strany přinesly to, co ta druhá nemohla. To je to skutečné poučení.