Migrace Go backendu — vývojářská reference
Migrace Go backendu — vývojářská reference
Section titled “Migrace Go backendu — vývojářská reference”sencai-backend je strangler-fig náhrada backendu pro Strapi CMS (sencai.space), napsaná v Go. Je to velmi rozsáhlý, aktivně vyvíjený program — tato stránka slouží k orientaci, pro plný detail po jednotlivých doménách čti přímo sencai-backend/CLAUDE.md a sencai-backend/CUTOVER-PLAN.md.
Proč tohle existuje
Section titled “Proč tohle existuje”Strapi (sencai.space) je původní backend platformy — rozsáhlý Node.js/TypeScript codebase (~200 content-types), který se stal na aktuální škále platformy nákladný na iteraci. sencai-backend re-implementuje stejný API povrch, content-type po content-type, za identickým HTTP kontraktem, aby se provoz mohl postupně přesouvat po jednotlivých routách (vzor “strangler fig”) místo přes jeden velký, rizikový rewrite-and-flip.
Jazyková historie
Section titled “Jazyková historie”Služba byla původně postavena v Rustu (Axum + Tokio + sqlx + lapin). 2026-07-05 bylo jazykové rozhodnutí obráceno na Go — byznysové rozhodnutí (rychlejší iterace, jednodušší provoz a shoda se dvěma existujícími Go službami v monorepu, agent-gateway/sencai-agent), ne technické selhání Rust buildu, který byl v době přepnutí CI-zelený a živě ověřený. Rust implementace nebyla smazána — zůstává dohledatelná v git historii a sloužila jako už ověřená referenční specifikace při portování domén, které měly rustovou obdobu (business logika a edge-case bylo potřeba vyřešit jen jednou).
Rozsah
Section titled “Rozsah”Původní PHASE-5-PLAN.md naplánoval tento projekt jako 7 modulů (FOUNDATION/TENANT/PROVISIONING/AUDIT/METERING/BILLING/DEPRECATE). Skutečný rozsah, jakmile práce začala, se ukázal být mnohem větší — plná parita napříč přibližně 200 Strapi content-types, oba Strapi RBAC systémy, media/uploads a admin panel, program srovnatelný škálou s 36 vlnami FÁZE 2. CUTOVER-PLAN.md sleduje tohle jako rozpad po vlnách (WAVE-1 až WAVE-39 v době psaní) a je živým zdrojem pravdy pro přesný stav po jednotlivých doménách — tato stránka popisuje tvar práce, ne vyčerpávající seznam po content-types.
- Portování content-types: velká většina Strapi content-types napříč všemi doménovými oblastmi (tenant/organisation core, cloud provisioning, cloud networking/IAM/assets/pricing, IaC/blueprints, resilience/chaos, registry/git, secrets/security, FinOps cost tracking, fleet core/ops/opsloop, observability/alerting/incidents, SLA/customer-health, compliance/GDPR/AI-Act governance, workspace directory sync, gamification/forum, marketplace a většina non-content-type custom API modulů jako
legal,privacy-portal,scim,operations,checkout,analytics,budgetasponsorships) byla portována, zrevidována a pokryta live-database regresními testy. - AUDIT je rozdělen na dvě části. Část A (read/verify/evidence/export/anchor povrch nad sdíleným, stále Strapi-owned hash chainem
audit_logs) je hotová. Část B — samotný cutover hash-chain writeru — je explicitně nezačatá a je human-gated, protože dva nezávislé writery soupeřící o stejný chain head by řetěz poškodily (viz “Single-writer invariant” níže). - DEPRECATE (postupné vyřazování odpovídajících Strapi rout, jakmile doména plně přejde) nezačalo pro žádnou doménu.
- Databáze:
sencai-backendnesdílí Strapi’sbackend-db. 2026-07-06 byla rozdělena do vlastní MySQL instance (go-backend-db), forknuté jednou zbackend-dbs migrací zachovávající ID. Neexistuje žádný automatický nástroj pro schema migrace — schémago-backend-dbbylo zkopírováno jednou a vyvíjí se přes ručně aplikované, revidované SQL soubory per package (dokumentované jakoschema_*.sqlartefakty v každém doménovém package), nikdy živá Strapi/Knex migrace. - Orchestrace: zapojena do
orchestration/pod profilemgo-backend(hostgo-backend.sencai.localhost, port 8080 interně) — shadow-only, záměrně vyloučena zDEFAULT_PROFILES/alltam, kde to rizikový profil úkolu vyžaduje (viz dedikované subsystémy níže). - Produkční nasazení: blokováno na živý
cdktf deployinfra-gcp, který sám ještě neproběhl. PROD-CUTOVER kroky a případné vypnutí Strapi jsou sledované v kořenovémmanual-steps.mda vyžadují samostatné, explicitní budoucí schválení — nic, co by mohl autorizovat implementační úkol sám o sobě.
Architektura
Section titled “Architektura”- Jeden Go binary per entrypoint, několik
cmd/*binárek sdílejících jeden modul:cmd/sencai-backend(hlavní HTTP API),cmd/healthcheck,cmd/dbsync-consumer,cmd/parity-harness,cmd/metering-consumer— všechny sestavené do stejnéhoscratch-based Docker image, vybrané přesENTRYPOINToverride per Compose služba. - Package-per-doména, ne package-per-vrstva: každá content-type/doména vlastní svůj
internal/<domain>/{handler.go,repo.go,types.go}, Go konvence oproti dřívějšímu rozpadu Rust buildu (domain//handlers//repositories). - Sdílené packages
internal/bez business logiky:authn(JWT/JWKS validace, ručně napsaná TTL+rate-limited JWKS cache),mq(RabbitMQ publisher s reconnect/backoff),orgscope(org-membership/role resolution sdílená každou doménou),tenant(cross-tenant autorizace/resolution),auditpub(fire-and-forget audit publish),problem(RFC 7807 chybové odpovědi),config/logging/db. - Žádná samostatná tenant/auth mikroslužba — tenant/organisation resolution žije přímo ve vlastních packages tohoto binary; samostatná služba by jen duplikovala jednobinary strukturu, která tu už je založená.
- Testovací konvence: čisté stdlib
testings table-drivent.Runsubtesty — žádnýtestify. Každá netriviální oprava jde ruku v ruce s live-database (a kde relevantní, live-broker) regresním testem, ne jen unit testem — viz “Inženýrská disciplína” níže.
Klíčové subsystémy
Section titled “Klíčové subsystémy”internal/dbsync — průběžný sync mechanismus
Section titled “internal/dbsync — průběžný sync mechanismus”Protože go-backend-db a Strapi’s backend-db jsou po splitu dvě nezávislé databáze, něco musí udržovat kopii na Go straně aktuální, dokud Strapi zůstává jediným systémem přijímajícím reálné zápisy. internal/dbsync (plus samostatná binárka cmd/dbsync-consumer) je tenhle mechanismus: event-triggered pull z existující exchange audit.events/audit.write, se zálohou periodické reconciliation celé tabulky. Čte backend-db přes dedikovaný, read-only MySQL credential (go_dbsync_reader — jen GRANT SELECT, vynucené na úrovni MySQL grantu, ne jen v aplikačním kódu) a zapisuje do go-backend-db.
Podle posledního počtu registruje dbsync-consumer adaptéry pro velkou většinu dbsync-eligible content-types platformy (organisation/cloud-instance/organisation-member a desítky dalších napříč billing, fleet, cloud, workspace, compliance a audit doménami) — pokrytí je blízko kompletnímu, ne úplné; hrstka tabulek zůstává nezapojená. Každý adaptér nikdy nedůvěřuje raw numerické cizí klíči zdrojového řádku napříč dvěma nezávisle forknutými auto-increment sekvencemi — relace se vždy znovu-vyřeší přes vlastní document_id cíle.
Per-resource-type vypínač (DBSYNC_DISABLED_RESOURCE_TYPES) existuje specificky proto, aby šel resource type vyloučit z živé sync cesty i z reconcileru dřív, než jakýkoliv reálný cutover zvedne váhu provozu té routy nad 0 % — tohle je dokumentovaný předpoklad v cutover runbooku, ne automatika.
dbsync-consumer je vlastní Compose profil, záměrně vyloučený z DEFAULT_PROFILES/all — nová infrastruktura čtoucí reálná produkčně tvarovaná data by nikdy neměla startovat jako vedlejší efekt holého up -d.
cmd/parity-harness — Fáze 6 kontinuální shadow-traffic parity
Section titled “cmd/parity-harness — Fáze 6 kontinuální shadow-traffic parity”Naivní přístup “diffni odpověď Strapi proti odpovědi Go pro stejný řádek” přestal být použitelný v momentě splitu databáze — obě backendy už nesdílí jeden podkladový řádek k porovnání a Strapi dál přijímá reálné zápisy, které Go nikdy nevidí. parity-harness místo toho vytváří vlastní canary řádky nezávisle na obou backendech ze stejného vstupního payloadu a ověřuje, že odpověď každého backendu odpovídá tomuto známému vstupu — metodika, která je konstrukčně imunní vůči driftu databáze.
Tři frekvenční tiery, zvolené podle reálné ceny side-effectu:
- fast (výchozí každých 10 minut) — organisation-member CRUD plus čtení organisation/organisation-member, samo-čistící, žádné reálné side effecty.
- medium (výchozí každé 4 hodiny, plus samostatná denní create-probe) — organisation update/rotace popisu, a vzácná disposable-org create-then-archive probe (protože
organisation.create()má reálné side effecty: Gitea provisioning, forum webhook, trial-state, funnel telemetrie). - slow (výchozí každých 24 hodin) — cloud-instance CRUD, zabezpečená double-key safety interlockem.
Safety interlock slow tieru existuje specificky kvůli reálnému incidentu z 2026-07-06, kdy credential-less cloud-instance create tiše provisionoval reálnou Hetzner VM proti reálnému cloud účtu platformy. Klíč 1: canary payload harness konstrukčně nikdy neobsahuje credential_id, takže fail-closed provisioning guard cloud-connector job rovnou odmítne. Klíč 2: celý slow tier je defaultně přeskočen, pokud není explicitně nastaveno PARITY_HARNESS_CONFIRM_NO_REAL_PROVISIONING=true — lidská atestace, kterou tento proces sám nemůže ověřit.
Součástí téhle dodávky je i Traefik-mirroring konfigurace, která duplikuje vzorek reálného GET-only produkčního provozu do Go backendu asynchronně (nikdy mutující request — mirrorování jednoho by způsobilo, že by Go provedl vlastní reálné side effecty na provozu, kterého se nikdy neměl dotknout), čistě pro signál stability/error-rate/latence, a Grafana dashboard parity-harness plus Prometheus alert pravidla.
parity-harness je vlastní Compose profil, vyloučený z DEFAULT_PROFILES/all ze stejného důvodu “nikdy nespouštět reálný canary provoz náhodou”, a vyžaduje jednorázový manuální bootstrap Keycloak uživatele před prvním spuštěním.
Dual-write burn-in pro metering (F5.METERING.04)
Section titled “Dual-write burn-in pro metering (F5.METERING.04)”internal/meteringsync + cmd/metering-consumer je druhá, nezávislá implementace odpovědnosti billing-adapter synchronizovat usage do Lago, běžící read-only proti backend-db přes vlastní dedikovaný credential go_metering_reader. Toto je stínový burn-in, ne cutover: billing-adapter zůstává jediným systémem, který skutečně označí usage event jako synced ve Strapi. Oba producenti vystavují Prometheus metriky pod stejnými jmény sérií (odlišené job labelem), aby šly srovnat na jednom dashboardu dřív, než padne jakékoliv rozhodnutí o cutoveru. Viz Billing Adapter pro druhou stranu tohoto srovnání.
internal/servicetoken — redesign credential modelu (Option B)
Section titled “internal/servicetoken — redesign credential modelu (Option B)”Capability-scoped, DB-backed, rotující token authority (hashovaná at-rest, plaintext odhalen jednou při vydání/rotaci) — Go-native odpověď na Strapi model admin-tokenu per service identita. První vlna zavírá reálnou mezeru pro git-connector’s bridge do tohoto backendu (statický sdílený secret dřív uděloval blanket delete práva na třech content-types, které původní Strapi grant nikdy nedal); druhá vlna dělá totéž pro jediné reálné volání cloud-connector do tohoto backendu (cloud-instance status callbacky). Obě strany přijímají existující statický secret i vydaný token additivně — žádný existující volající kód se nemusel měnit.
internal/useravatar — první reálný media/uploads subsystém
Section titled “internal/useravatar — první reálný media/uploads subsystém”Úzký, jen-avatar self-service upload/resize/serve subsystém (ne Go port Strapi’s generického polymorfního systému files) — lokální disk storage odpovídá tomu, co Strapi sám dnes reálně provozuje, se třemi záměrně menšími breakpointy než Strapi’s vlastních pět. Zajímavý kvůli provozním gotchas, které odhaluje pro kohokoliv, kdo přidává druhou upload funkci: kontejner na bázi scratch nemá defaultně zapisovatelný temp adresář (limity velikosti multipart body musí být vyladěné tak, aby parser nikdy nespilloval na disk), a plošný cap velikosti request body v routeru potřeboval úzkou, route-specifickou výjimku místo globálního zvýšení.
Inženýrská disciplína
Section titled “Inženýrská disciplína”Několik vzorů se opakuje napříč téměř každou doménou portovanou do tohoto backendu a stojí za to je znát před čtením (nebo rozšiřováním) kterékoliv z nich:
- “Zápis do DB ≠ reálná akce.” Před portováním jakékoliv akce, která vypadá, že spouští něco destruktivního (spuštění runbooku, chaos experiment, fleet karanténa, dispatch remote commandu), každá vlna trasovala celý reálný řetěz end-to-end, aby zjistila, jestli momentálně, reálně funguje v živém systému. Několik z nich reálně nefunguje (chybějící WebSocket message handler, špatný typ exchange, hardcoded mrtvý port) — pro ty tento backend záměrně provede stejný zápis do databáze a audit event, jaké by provedl reálný systém, ale nikdy se nepokusí o skutečný dispatch, a upřímně to zveřejní jak v audit eventu, tak v HTTP odpovědi jako
dispatch_deferred: true. Není to lenost — obnovení momentálně rozbitého destruktivního triggeru bez přidání pojistky by udělalo z nově-fungujícího endpointu ten s nejvyšším blast radiusem ve službě. - Revize cross-tenant IDOR je standard, ne výjimka. Velmi velké množství jednotlivých portovacích vln nalezlo a uzavřelo reálné, živé mezery v access-control v původních Strapi controllerech po cestě — role floors s pouhým bare-membership zpřísněny, existence oracles 403-vs-404 sjednoceny, mass-assignment na pole
organisationuzavřen. Tohle je dokumentované per-vlna vsencai-backend/CLAUDE.md; nepředpokládat, že existující chování Strapi je bezpečný výchozí bod k reprodukci bez ověření. - TOCTOU race conditions se uzavírají přes
SELECT ... FOR UPDATEnebo MySQL named lock (GET_LOCK), každá zálohovaná skutečným multi-goroutine concurrency testem dokazujícím opravu pod reálnou zátěží, ne jen sekvenčním double-call checkem. - Secrety nikdy neprocházejí generickým response DTO. Každý content-type nesoucí credential (BYOC cloud credentials, SSH klíče, service tokeny, agent enrollment tokeny) používá response typ, který konstrukčně nemůže nést pole se secretem, místo aby se spoléhal na konvenci vynechat ho.
Single-writer invariant (audit hash chain)
Section titled “Single-writer invariant (audit hash chain)”Append-only, hash-chained audit log platformy (audit_logs) musí mít přesně jednoho writera, jinak dva procesy soupeřící o chain head řetěz poškodí. Dnes je tímto writerem JS audit-consumer uvnitř auth-service-consumer. Go port stejného writeru (audit-consumer, dřív audit-consumer-rs) existuje, je CI-zelený a otestovaný — ale přepnutí writeru (vypnutí JS consumeru, přehrání backlogu audit.fallback eventů, zapnutí Go varianty) je záměrně samostatný, human-gated cutover krok, sledovaný jako AUDIT Část B. Nic z content-type portovací práce výše se tohoto nedotýká.
Build a testy
Section titled “Build a testy”Tento projekt obecně předpokládá, že na hostiteli není Go toolchain — použij vlastní scripts/go-docker.sh <go subcommand> repozitáře, který spustí go uvnitř kontejneru golang:1.25-alpine s cachovanými module/build volumes a Docker network přístupem k dev stacku. Testy, které potřebují živou dostupnost databáze, nastavují EXTRA_NETWORK=sencai_db-go-backend (a sencai_db-backend tam, kde test potřebuje číst i vlastní databázi Strapi, např. testy dbsync/parity-harness/meteringsync).
Kam dál
Section titled “Kam dál”sencai-backend/CLAUDE.md— plný, průběžně aktualizovaný log stavu po vlnách (rozsáhlý — tato stránka je jeho destilát)sencai-backend/CUTOVER-PLAN.md— rozpad content-types po vlnách a tabulka cutover-readiness po jednotlivých routáchPHASE-5.5.md(kořen repozitáře) — živý plánovací dokument pro to, co zbývá: AUDIT Část B, produkční cutover meteringu, zbývající pokrytídbsync, zbylé identity redesignu servicetoken, burn-in parity harnessu a případná budoucí náhrada dual-RBAC/admin panelu