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>
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_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 |