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>
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_lockby 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.
vje 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 |