Files
csbot-prototype/documentation/14-databaze.md
T
JiriUhlirandClaude Opus 5 a771834e57 Realne sluzby, OpenAI, odesilani e-mailu a helpdesk
Katalog srovnany s tim, co opravdu bezi na services.csbot.cz/apps:
trinact sluzeb dostalo pristupove udaje a levne cteci overeni, opravena
appId, ktera nikam nevedla (ppl, microsoft365, transcription), a GA4,
Search Console, Google Ads i Sklik ted stoji na aplikaci analytics,
kazda s vlastnimi udaji. Nove sluzby SAP Business One, Google Workspace
a Meta Ads. K tomu 23 skriptu, ktere s nimi opravdu neco delaji.

OpenAI jako prvni sluzba, ktera nebezi u nas: Service.baseUrl s absolutni
adresou, prepis pres <SLUZBA>_BASE_URL nebo adresu u konektoru, predpona
hlavicky u pole udaju (uzivatel vlepi holy klic, Bearer dopise runtime).
Dotaz na model, nahrani souboru, otazka nad souborem, prepis zvuku.
Skript umi odeslat soubor pres ctx.http.postForm (multipart, obsah Base64).

Sluzba E-mail pres SMTP. Neni to skript, ale vnitrni krok - SMTP neni HTTP.
Konektor nese schranku firmy, krok ma HTML telo, ve kterem se dosazene
hodnoty escapuji (znacky autora sablony jsou zamer, ostre zavorky od
zakaznika ne). Overeni konektoru se prihlasi na server a nic neodesle.

Helpdesk: Ticket.helpdeskSourceId drzi firmu, ktera pozadavek poslala,
vlastnikem zustava ta, ktera ho resi - jinak by ho resitel nemel ve sve
fronte. Komu pozadavek pripadne, urcuje Tenant.helpdeskProviderId.
Zadavatel vidi jen svoje pozadavky a smi k nim pripsat komentar.

Opravy v portalu:
- hlasky o ulozisti a odchozi IP vidi jen spravce platformy
- typ ticketu se v automatizaci vybira ze seznamu firmy, nebo dosadi z dat
- stav ticketu je otevreny naseptavac, ne ciselnik
- ticket jde zalozit rucne, zakaznik u nej neni povinny
- kanal se prejmenoval a parametry u webhooku jsou oznacene jako nepovinne
- srovnane markdown tabulky v cele dokumentaci

Co z teto davky jeste neni: prepinac firmy je porad jen stav uvnitr stranky
Prehled, takze se prepnuti neprojevi v Lidech ani jinde.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 07:40:16 +02:00

11 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