Files
csbot-prototype/documentation/14-databaze.md
T
JiriUhlirandClaude Opus 5 78e7f99d60 Konektory do Postgresu, pristupove udaje sifrovane
Pristupove udaje konektoru se ukladaji do databaze a prezijou restart. Popis
v documentation/14-databaze.md.

Databaze je volitelna a rezimy jsou oddelene:
- postgres kdyz je DATABASE_URL i SECRETS_KEY
- memory jinak, tedy pri nasazenem mockupu a lokalnim vyvoji bez DB

Rozhodnuti je jen na jednom miste (src/data/connectorStore.ts). Nikde jinde se
nezjistuje, jestli databaze je - kdyby se to rozlezlo po kodu, jedno misto by se
zapomnelo a chovalo by se pak jinak nez zbytek.

Chybejici databaze nesmi shodit start: container, ktery nenastartuje, je pro
AppFactory nefunkcni sluzba. Misto toho se do logu napise proc a portal to ukaze
na strance Konektory. Stejne tak kdyz migrace selzou - psat do rozbiteho
schematu je horsi nez neukladat.

Databaze potrebuje oboji. Bez SECRETS_KEY by se udaje ukladaly v plaintextu
a to je horsi nez ztratit je pri restartu: tabulku vidi kazda zaloha a kazdy
dump pri ladeni.

Pridano:
- pool v src/db/pool.ts vcetne transakci a dbFor(tenantId) jako sev pro budouci
  oddelenou databazi jednoho klienta
- migrace ze src/db/migrations/*.sql pod pg_advisory_lock, jinak je pri rolling
  deployi pusti vsechny instance naraz. Jeden soubor je jedna transakce
- sifrovani AES-256-GCM s nahodnym IV a verzi klice. Nerozsifrovatelna hodnota
  nepada, chova se jako nevyplnena a zaloguje se - jeden rozbity konektor nesmi
  shodit seznam ostatnich
- /health/ready s pingem do DB. /health na databazi zamerne nezavisi, kratky
  vypadek by jinak vedl k restartovani containeru
- GET /api/dashboard/storage a hlaska v portalu o tom, ze data jsou jen v pameti
- jediny vychozi konektor na firmu a sluzbu hlida castecny unikatni index, ne jen
  kod. Dva soubezne zapisy by jinak udelaly dva vychozi

Zmeneno: cteni i zapis konektoru je asynchronni, vcetne validace stromu.

Overeno proti Postgresu 16 v kontejneru: migrace, sifrovani v tabulce, preziti
restartu, rozsifrovani spravnym klicem, degradace pri spatnem klici, PATCH bez
tajneho pole, prepnuti a smazani vychoziho konektoru, pametovy rezim bez
DATABASE_URL. Kontejner po overeni smazan.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 15:08:25 +02:00

7.9 KiB

14 - Databaze

Naprogramovano a overeno proti Postgresu 16. Zatim se do databaze ukladaji konektory, tedy pristupove udaje k sluzbam. Zbytek je v pameti procesu, poradi dalsich kroku je na konci.

Databaze je volitelna, ale rezimy jsou oddelene

Aplikace jede ve dvou rezimech a rozdil se resi na jednom miste, v src/data/connectorStore.ts. Nikde jinde se nezjistuje, jestli databaze je - kdyby se to rozlezlo po kodu, jedno misto by se zapomnelo.

Rezim Kdy Prezije restart
postgres je DATABASE_URL i SECRETS_KEY ano
memory jinak ne

Chybejici databaze nesmi shodit start. Container, ktery nenastartuje, je pro AppFactory nefunkcni sluzba (AGENTS.md). Misto toho se do logu napise, proc se jede v pameti, a portal to ukaze na strance Konektory.

Databaze potrebuje oboji. Bez klice by se pristupove udaje ukladaly v plaintextu, a to je horsi nez ztratit je pri restartu - tabulku vidi kazda zaloha a kazdy dump pri ladeni.

Stejne tak: kdyz jsou migrace nastavene, ale selzou, jede se dal v pameti. Psat do rozbiteho schematu je horsi nez neukladat.

Promenne

Promenna K cemu
DATABASE_URL postgres://uzivatel:heslo@host:5432/csbot
SECRETS_KEY klic pro sifrovani pristupovych udaju, secret
DATABASE_POOL_MAX kolik spojeni si vezme jedna instance, vychozi 10
DATABASE_SSL true u spravovanych databazi, ktere vyzaduji TLS

SECRETS_KEY ma byt nahodny retezec, ne heslo:

node -e "console.log(require('crypto').randomBytes(32).toString('base64url'))"

Klic se nesmi ztratit ani zmenit bez prevodu dat. Bez nej se ulozene udaje nerozsifruji. Nic se nerozbije, jen se konektory chovaji jako nevyplnene a v logu je napsane proc - udaje se pak zadaji znovu.

Lokalni spusteni

docker run -d --name csbot-pg -p 5433:5432 \
  -e POSTGRES_PASSWORD=devpass -e POSTGRES_DB=csbot postgres:16-alpine

export DATABASE_URL="postgres://postgres:devpass@127.0.0.1:5433/csbot"
export SECRETS_KEY="$(node -e "console.log(require('crypto').randomBytes(32).toString('base64url'))")"
npm run dev

Migrace se pousti samy pri startu. Kontrola, ze to jede z databaze:

curl -s http://localhost:3000/health/ready

Bez promennych npm run dev funguje dal, jen v pameti.

Migrace

Soubory src/db/migrations/*.sql, v abecednim poradi, kazdy jednou. Co uz proslo, je v tabulce schema_migrations.

Dve veci, na kterych to stoji:

  • Poradovy zamek. Pri rolling deployi startuje vic instanci naraz a bez pg_advisory_lock by migrace pustily vsechny.
  • Jeden soubor je jedna transakce. Pri chybe se z nej neuplatni nic, takze nevznikne rozdelane schema, o kterem nikdo nevi.

Migrace se nikdy neupravuji zpetne. Uz projely u nekoho jineho, takze zmena souboru znamena dve rozdilna schemata se stejnym cislem. Oprava je vzdy novy soubor.

Sifrovani pristupovych udaju

src/db/secretBox.ts, AES-256-GCM.

{ "v": 1, "iv": "...", "tag": "...", "data": "..." }
  • GCM, ne CBC: sifruje a zaroven overuje, ze s daty nikdo nehybal.
  • Nahodne IV pro kazdou hodnotu, aby dve stejne hodnoty nedaly stejnou sifru.
  • v je verze klice. Vymena klice pak znamena precist starym a zapsat novym, ne zahodit vsechna napojeni.
  • Nerozsifrovatelna hodnota nepada. Jeden rozbity konektor nesmi shodit seznam vsech ostatnich, takze se chova jako nevyplneny a zaloguje se to.

Sifruji se vsechna pole, i necitliva. Je to jednodussi nez rozhodovat u kazdeho zvlast a nic to nestoji.

Schema

connectors (migrace 001_connectors.sql):

Sloupec Poznamka
tenant_id povinne, index zacina jim
service_id odkaz do katalogu v kodu, ne do tabulky
secrets JSONB se sifrovanymi hodnotami, nikdy plaintext
is_default jediny vychozi na firmu a sluzbu, hlida index

Sluzby v databazi nejsou. Jsou to definice, ktere delame my, a repo je u nich zdroj pravdy kvuli code review a historii v gitu. Rucne upraveny radek v produkci nikdo za tri mesice nedohleda. Duvody jsou v 12-sluzby-a-konektory.md.

Vychozi konektor hlida castecny unikatni index, ne jen kod:

CREATE UNIQUE INDEX connectors_one_default_idx
  ON connectors (tenant_id, service_id) WHERE is_default;

Bez nej by dva soubezne zapisy udelaly dva vychozi a krok bez vybraneho konektoru by si vybiral podle nahody.

Dvere k oddelene databazi

dbFor(tenantId) dnes vraci vzdy tentyz pool. Je to zamerny sev: jednou prijde klient, ktery bude chtit vlastni databazi nebo bude delat tricet procent provozu, a presun ma byt konfigurace, ne prepisovani dotazu.

Podminka je nikdy nespojovat dotazem dva klienty, coz uz vynucuje povinny argument tenantIds v ulozistich. Podrobnosti v 10-runtime-a-kapacita.md.

Health

Endpoint Zavisi na DB K cemu
/health ne liveness, AppFactory podle nej restartuje
/health/ready ano readiness, vraci 503 pri nedostupne DB

/health nesmi na databazi zavisel. Kratky vypadek DB by jinak vedl k restartovani containeru, coz nic nespravi. Vysledek pingu se par sekund cachuje, aby monitoring nedelal dotaz pri kazdem pingu.

Pool a jedno pravidlo

DATABASE_POOL_MAX je vychozi 10 a je to zamerne malo. Worker nesmi drzet spojeni po dobu volani ciziho API - volani do iDokladu trva 300 ms a pri stovce soubeznych kroku by to bylo sto obsazenych spojeni. Se spravnym poradim (odeber ulohu, uvolni spojeni, volej, zapis) staci par.

Az bude instanci vic, prijde PgBouncer v transakcnim rezimu. Pozor: v nem nefunguje LISTEN/NOTIFY, na kterem ma stat sbernice udalosti pro SSE. Ta pak potrebuje prime spojeni mimo PgBouncer.

Co bylo overeno

Proti Postgresu 16 v kontejneru:

Co Vysledek
Migrace projedou a zapisou se do schema_migrations ano
Udaje jsou v tabulce sifrovane, plaintext nikde ano
Konektor prezije restart procesu ano
Se spravnym klicem se udaje rozsifruji ano
Se spatnym klicem se chovaji jako nevyplnene a loguje se ano
PATCH bez tajneho pole tajne pole nesmaze ano
Prepnuti vychoziho konektoru ano
Smazani vychoziho preda priznak zbylemu ano
Bez DATABASE_URL jede pametovy rezim a rekne to ano

Co chybi

Chybi Poznamka
Automatizace v databazi dalsi na rade, je to to, co si clovek nastavi
Rozlozeni dashboardu male a samostatne, hned po automatizacich
Tickety a incidenty naposled, dnes je generuje simulace
Uzivatele, firmy, resitele v prototypu je to spis konfigurace nez data
Sbernice udalosti pres LISTEN/NOTIFY dnes EventEmitter v pameti jedne instance
Vymena klice (rotace) v je pripravene, prevod dat napsany neni
Retence a partitionovani az u tabulek behu, viz dokument 10