Files
csbot-prototype/documentation/14-databaze.md
T
JiriUhlirandClaude Opus 5 6e3d0640ff Soubor jako uloziste, kdyz neni databaze
Mockup se k databazi nedostane, takze pribyl treti rezim: JSON soubor. Prezije
restart procesu i containeru, ale ne redeploy - filesystem containeru je
docasny. Je to mezistupen, ne nahrada databaze, a tak je to i napsane v portalu.

| Rezim    | Kdy                               | Restart | Redeploy |
| -------- | --------------------------------- | ------- | -------- |
| postgres | DATABASE_URL i SECRETS_KEY        | prezije | prezije  |
| file     | neni DB, ale je DATA_DIR          | prezije | ne       |
| memory   | ani jedno, nebo nejde zapsat      | ne      | ne       |

Rozhodnuti zustava na jednom miste (src/data/connectorStore.ts).

Pridano:
- src/data/snapshot.ts: atomicky zapis (.tmp a prejmenovani), slucovani zapisu
  a dokonceni rozepsaneho zapisu pri SIGTERM. Bez atomickeho zapisu by pad
  uprostred nechal polovicni JSON, ktery se pri startu nenacte. Rozbity soubor
  se prejmenuje na .broken a jede se dal - aplikace, ktera nenastartuje, je pro
  AppFactory nefunkcni sluzba
- src/data/connectors/local.ts: jeden kod pro pamet i soubor, lisi se jen tim,
  kam se zapisuje. Nahrazuje memory.ts, dve implementace by se casem rozesly
- klic k sifrovani se mimo databazi vygeneruje do DATA_DIR/secrets.key s pravy
  0600, takze sifrovani funguje bez nastaveni. Chrani to proti nahodnemu
  precteni JSONu, ne proti pristupu k disku - klic lezi vedle dat a je to tak
  napsane i v portalu. U databaze se negeneruje vubec: kdo ma zalohu tabulky,
  ma i klic ze stejneho stroje
- DATA_DIR v konfiguraci, data/ v .gitignore a .dockerignore
- hlaska v portalu rozlisuje tri nasledky: pamet, soubor a databaze

Overeno bez databaze: konektor s vyplnenymi udaji prezil restart, v JSONu jsou
hodnoty sifrovane a plaintext v nem neni. Pote s databazi: rezim postgres
funguje dal a klic vedle dat se nevygeneroval. Kontejner i data/ po overeni
smazany.

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

10 KiB

14 - Databaze

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

Ukladat jde tremi zpusoby a rezim se vybira sam podle toho, co je k dispozici.

Tri rezimy, jedno rozhrani

Rozdil se resi na jednom miste, v src/data/connectorStore.ts. Nikde jinde se nezjistuje, ktery rezim jede - kdyby se to rozlezlo po kodu, jedno misto by se zapomnelo a chovalo by se pak jinak nez zbytek.

Rezim Kdy Prezije restart Prezije redeploy
postgres je DATABASE_URL i SECRETS_KEY ano ano
file neni databaze, ale je DATA_DIR ano ne
memory ani jedno, nebo nejde zapsat ne ne

Rezim file je pro mockup. Filesystem containeru je docasny, takze soubor prezije restart procesu i containeru, ale nove nasazeni ho smaze. Je to mezistupen, ne nahrada databaze.

Nasazeny mockup tedy jede v rezimu file a v portalu je to napsane. Lokalne s Postgresem jede postgres.

Chybejici databaze nesmi shodit start. Container, ktery nenastartuje, je pro AppFactory nefunkcni sluzba (AGENTS.md). Misto toho se do logu napise, ktery rezim jede a proc, 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 bez databaze. Psat do rozbiteho schematu je horsi nez psat do souboru.

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
DATA_DIR slozka pro JSON mimo databazi, vychozi ./data. Prazdna hodnota vypne i soubor

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 se uklada do ./data.

Soubor misto databaze

src/data/snapshot.ts a src/data/connectors/local.ts.

Ukladani je jeden kod pro pamet i soubor, lisi se jen tim, kam se zapisuje. Kdyby to byly dve implementace, jedna by se casem opravila a druha ne.

Vlastnost Jak a proc
Atomicky zapis nejdriv .tmp, pak prejmenovani. Pad uprostred zapisu jinak nechá polovicni JSON, ktery se pri startu nenacte
Slucovani zapisu deset uprav za sebou znamena jeden zapis na disk
Zapis pri ukonceni SIGTERM dokonci rozepsany zapis, jinak by se posledni zmena ztratila
Rozbity soubor prejmenuje se na .broken, zaloguje a jede se s prazdnymi daty. Aplikace, ktera nenastartuje, je pro AppFactory nefunkcni sluzba
Sifrovani tajne hodnoty jsou v souboru zasifrovane, plaintext nikdy

Klic mimo databazi

Bez SECRETS_KEY si aplikace v rezimu file vygeneruje klic do DATA_DIR/secrets.key (prava 0600). Diky tomu funguje sifrovani bez jakehokoliv nastaveni.

Rekneme si nahlas, co to je a co ne. Klic lezi ve stejne slozce jako data, takze to chrani proti nahodnemu precteni JSONu, ne proti nekomu, kdo ma pristup k disku serveru nebo k zaloze slozky. Do provozu patri klic ze secretu, tedy SECRETS_KEY.

U databaze se klic vedle dat negeneruje vubec. Nemelo by to smysl: kdo ma zalohu tabulky, ma i klic ze stejneho stroje. Proto je allowKeyFile parametr, ne automatika.

Kdyz nejde ani jedno, tajne hodnoty se do souboru neukladaji a zbytek konektoru ano. Radsi je zadat znovu nez je mit v souboru citelne.

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 souborovy rezim a rekne to ano
Konektor v souborovem rezimu prezije restart procesu ano
Tajne hodnoty jsou v JSONu sifrovane, plaintext nikde ano
U databaze se klic vedle dat nevygeneruje ano

Co chybi

Chybi Poznamka
Automatizace v ulozisti dalsi na rade, je to to, co si clovek nastavi. Pujde do souboru i do databaze
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