S3 Data Exchange — unifikovaná datová rovina AVAX¶
Status: Draft 1.0 · 2026-06-06 · ultrathink spec Sjednocuje + rozšiřuje:
s3-architecture.md,sync-backup.md,chat.md,android-call-capture.md,android-v0.5.md, per-app S3 provisioning. Cíl: JEDNA S3-centrická async vrstva pro veškerý přenos dat — device↔cloud (Android/desktop folder sync), app↔app handoff (hotline-capture→ hotline a desítky dalších), user↔user (chat). Místo 3 ad-hoc reimplementací.
0. Proč (motivace)¶
Dnes existují tři oddělené S3 mechanizmy se STEJNÝM principem:
| Mechanizmus | Spec | Vzor |
|---|---|---|
| Chat | chat.md |
avax-chat bucket, JSON-per-message soubory, pending_deliveries DB queue + adaptive polling |
| Folder sync | sync-backup.md |
manifest.json + checksums + delta + snapshots (desktop) |
| Hotline capture | android-call-capture.md |
Android folder → App#2 Uploader → S3 (STS) → app-hotline ingestion |
Plus app-owned data, kde appky řeší S3 ad-hoc (app-hotline reuse APPDIST creds).
Princip je všude stejný — S3 = store, DB queue = notifikace, polling = doručení — jen 3× reimplementovaný. Tahle spec to sjednocuje a rozšiřuje o Android device sync + generic app↔app handoff.
Závazný princip (z chat.md, držet): ŽÁDNÝ WebSocket server, ŽÁDNÝ Redis
Pub/Sub. S3 store + DB notif queue (pending_* / exchange_items) + adaptive
polling. Škáluje na 100 000+ uživatelů při nízké HW zátěži (důvod původního
rozhodnutí pro chat).
1. Kde data leží — roviny úložiště¶
| Rovina | Umístění | Píše | Čte | Auth |
|---|---|---|---|---|
| User private | companies/{cid}/users/{uid}/… |
user (device) | user; backend on-behalf | STS prefix-scoped |
| Company shared | companies/{cid}/shared/{all\|roles\|custom/{did}}/ |
dle ACL | dle ACL | STS |
| App-owned | companies/{cid}/apps/{slug}/data/ (nebo dedikovaný app-{slug} bucket) |
app backend | app backend | M2M static creds |
| Exchange / handoff | companies/{cid}/exchange/{target}/inbox/{source}/… |
producer (app/device/user) | target app ingestion | STS (producer) / static (consumer) |
| Chat (special case) | avax-chat bucket (cross-company, SSE) |
chat-service | chat-service | system IAM |
Bucket-per-firma zůstává základ (s3-architecture.md); chat má vlastní
cross-company bucket (je cross-company); exchange je per-company (izolace).
2. ROZHODNUTÍ — odpovědi na klíčové otázky¶
2.1 „Firemní složka uživatele NEBO bucket?" → SLOŽKA (prefix), NE bucket¶
Android sync cíl = prefix v company bucketu: companies/{cid}/users/{uid}/
sync/<folder>/. Bucket-per-FIRMA, ne per-user.
Proč (per s3-architecture.md):
- GDPR výmaz firmy = drop 1 bucket (ne hon na 1000 user bucketů).
- Kvóta + billing per firma.
- Ceph/S3 nemá rád 100k+ bucketů; prefixů zvládne neomezeně.
- Cross-user sdílení (shared/) ve stejném bucketu = triviální.
User izolace je stejně silná přes STS — token scoped na users/{uid}/
prefix nemůže číst cizí adresář. Bucket-per-user by bezpečnost nezlepšil.
2.2 Jak Android dostane S3 (mobile-auth → STS)¶
- Pairing (
android-v0.5.md §7.1): launcher2 desktop → Settings → Devices → „Pair Android" → pairing JWT (QR 256×256, exp 10 min, scopedevice_pairing) → Android scan (CameraX+MLKit) →POST /v1/devices/register {pairing_jwt, device_info}→mobile_devicerow + refresh_token (Android Keystore / EncryptedSharedPreferences) + access_token. - STS per sync: Android →
GET /storage/token?scope=sync&folder=<name>(Bearer access_token) → STS scoped nausers/{uid}/sync/<name>/(TTL 1 h) → přímý S3 upload/download (bez proxy přes backend).
→ Nikdy permanentní S3 klíč na telefonu. Vše STS, max 1 h.
2.3 Auth matrix¶
| Kdo | Token | S3 přístup |
|---|---|---|
| Desktop/Android user | JWT (launcher IPC / mobile refresh) → STS | prefix-scoped, 1 h |
| App backend — vlastní data | M2M → static app-data creds (.env) |
app prefix / bucket |
| App backend — ingestion z exchange | M2M → STS scope=exchange (nebo company creds) |
exchange inbox |
2.4 Test QR pairing (S3 creds → mobil) — vývojový bootstrap¶
Pro rychlé testování Android S3 přístupu (než je hotový plný mobile-auth STS pairing §2.2): launcher2 → Můj profil → „Spárovat mobil (S3 — TEST)" vygeneruje PIN-šifrovaný QR s company S3 creds + user prefix. Mobil naskenuje + zadá PIN → dešifruje → drží zašifrovaně v Android Keystore, plaintext jen v paměti.
Flow:
1. Launcher: GET /storage/mobile-pairing-creds (admin gate) → {endpoint,
bucket: backup-{ico}, prefix: users/{uid}/, access_key, secret_key, user_id,
company_id}.
2. Launcher: PIN (6 číslic) + mobile_pairing.build_pairing_payload(creds, pin)
→ QR text avaxs3:<b64url>. PIN zobrazen ZVLÁŠŤ, ne v QR.
3. Mobil: scan → user zadá PIN → dekóduj (kontrakt) → creds → re-encrypt pod
Keystore klíčem → EncryptedSharedPreferences → AWS SDK na prefix.
Crypto kontrakt (reference: desktop/launcher2/mobile_pairing.py, self-test OK):
- KDF: PBKDF2-HMAC-SHA256, 200 000 iter, salt 16 B → 32 B klíč.
- Cipher: AES-256-GCM, nonce 12 B, bez AAD.
- Obálka: {v:1, kdf, iter, salt, nonce, ct} (b64url bez padding); QR = avaxs3: + b64url(obálka). ~458 B.
Android (Kotlin) dekodér (parita):
// data.removePrefix("avaxs3:") → Base64.urlSafe → JSON env
// key = PBKDF2WithHmacSHA256(pin, salt, iter=200000, keyLen=256)
// AES/GCM/NoPadding .doFinal(ct) s nonce → creds JSON
// → EncryptedSharedPreferences (MasterKey AES256_GCM)
⚠️ Proč TEST-only:
- Nese permanentní company creds (širší než STS) — únik = celý company bucket.
Produkce = STS scoped na users/{uid}/ (§2.2).
- 6-místný PIN je slabý proti offline brute-force zachyceného QR (PBKDF2 200k zvedá
laťku, 10^6 v dosahu). QR krátkodobý, nikde neuložený — nefotit.
- Gate: jen company_admin / super_admin.
→ Pro vývoj/pilot OK; před produkcí přepnout na §2.2 STS pairing.
3. Device folder sync (Android + desktop)¶
Manifest-based protokol (sync-backup.md §215+), zobecněný pro libovolnou
složku a Android.
3.1 Cíl v S3¶
companies/{cid}/users/{uid}/sync/<folder-slug>/
├── manifest.json ← stav: device_id, version, files[]{path,sha256,size,mtime}, snapshot ref
├── files/<relativní cesta> ← obsah
└── .snapshots/<ts>/manifest.json ← historie (rollback)
3.2 Sync cyklus¶
- Lokální manifest → spočítej checksums změněných souborů.
- Stáhni S3
manifest.json. - Diff per soubor:
- S3 novější → download.
- Lokální novější → upload delta → zapiš nový manifest → snapshot.
- Oba změnili (konflikt) → uchovej obě (
<name>.conflict-<device>), user resolve (persync-backup.md §263). - Atomicita: nový
manifest.jsonse zapíše POSLEDNÍ = commit marker. Konzument nikdy nečte half-uploaded stav.
3.3 Android specifika¶
FileObserverna watched folder → foreground service → offline queue (bez sítě čeká) → STS refresh → upload.- Ready-marker: soubor uploadni až je stabilní (size unchanged ~3 s) — stejně
jako hotline-capture
<id>.m4a.partial→atomic rename. - Battery/data: WiFi-only toggle, batch, exponential backoff.
- Refresh access_token před expirací (refresh_token z Keystore).
3.4 Sync-options katalog (co smí telefon zálohovat) — _options.json¶
Telefon v test-pairingu má jen S3 creds, NE JWT → nemůže volat katalog API. Proto backend předgeneruje nabídku kategorií jako JSON na S3, telefon si ho stáhne svými creds a zobrazí jako zaškrtávací seznam v Nastavení.
Cíl v S3: users/{uid}/sync/_options.json (v company bucketu, user-private
prefix; telefon ho čte přes spárované creds).
Producer: backend POST /storage/sync-options/publish (admin gate přes
get_company_credentials). Bere standardní kategorie + přístupné aplikace
uživatele (get_catalog_for_company → required_roles + active subscription
filter) → každá přístupná app = kategorie kind:"app" s default_checked=true.
Launcher2 ji volá při QR pairingu (creds + options se publikují společně).
Schéma sync-options/v1:
{
"schema": "sync-options/v1",
"user_id": "<uuid>", "company_id": "<uuid>",
"generated_at": "2026-06-06T…Z",
"categories": [
{ "id": "photos", "label": "Fotky", "kind": "media",
"default_checked": false, "suggested": "DCIM/Camera", "s3_subdir": "photos" },
{ "id": "service-settings", "label": "Nastavení služeb", "kind": "settings",
"default_checked": false, "s3_subdir": "service-settings" },
{ "id": "app-<slug>", "label": "<Name> — zálohy", "kind": "app",
"app_slug": "<slug>", "has_access": true,
"default_checked": true, "s3_subdir": "apps/<slug>" }
]
}
kind: media (fotky/galerie) · settings (konfig služeb) · app (záloha
adresáře konkrétní AVAX appky — předzaškrtnuto když má user přístup).
- s3_subdir: relativní cíl pod users/{uid}/sync/ (= <folder-slug> z §3.1).
- suggested: výchozí device cesta (telefon ji předvyplní v SAF pickeru).
Consumer (Android Nastavení): stáhne _options.json → checklist
(předzaškrtnuto dle default_checked) → per zaškrtnutá kategorie SAF folder
picker (ACTION_OPEN_DOCUMENT_TREE) → uloží mapping {category_id → tree URI}
do DataStore. Sync (§3.2) pak iteruje vybrané kategorie do
users/{uid}/sync/<s3_subdir>/.
4. App↔App data exchange (handoff / inbox) — JÁDRO¶
Zobecnění hotline-capture→hotline a chat pending_deliveries do jednoho
reusable vzoru pro „spoustu aplikací co si synchronizují data do aplikací".
4.1 Inbox vzor (producer → S3 → consumer ingestion)¶
Producer (app A backend / device / user) předává data target appce B:
- Zapíše do
companies/{cid}/exchange/{B}/inbox/{source}/{item_id}/: payload.<ext>(audio, DWG, JSON, export…)meta.jsonzapsán POSLEDNÍ (ready-marker — atomicita).- Registruje exchange item (notifikace) — jeden ze dvou módů:
- Pull (default): INSERT do
exchange_items(pending) → B polluje. - Push (volitelně):
POST /apps/{B}/v1/ingest/notify(M2M). - Consumer (B ingestion worker): claim item → stáhni payload → zpracuj →
přesun do
processed/{item_id}/(nebo delete) → statusdone.
4.2 meta.json (ready-marker — drž stabilní)¶
{
"schema": "exchange/v1",
"item_id": "uuid",
"source_app": "app-hotline-capture", "target_app": "app-hotline",
"company_id": "uuid", "user_id": "uuid",
"payload_file": "payload.m4a", "payload_schema": "hotline-capture/v1",
"size_bytes": 0, "checksum_sha256": "…", "created_at": "iso8601"
}
meta.json (payload zapsán dřív).
4.3 exchange_items (DB notif queue — analog chat pending_deliveries)¶
CREATE TABLE exchange_items (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
company_id UUID NOT NULL,
source_app VARCHAR(100) NOT NULL,
target_app VARCHAR(100) NOT NULL,
s3_key TEXT NOT NULL, -- …/inbox/{source}/{item_id}/
item_meta JSONB NOT NULL,
status VARCHAR(20) DEFAULT 'pending', -- pending|processing|done|failed
created_at TIMESTAMPTZ DEFAULT now(),
picked_at TIMESTAMPTZ, done_at TIMESTAMPTZ, error TEXT
);
CREATE INDEX idx_exch_target ON exchange_items (target_app, status, company_id);
GET /v1/exchange/next?target=B (M2M) →
claim (status→processing, FOR UPDATE SKIP LOCKED) → process → POST /v1/exchange/
{id}/ack (done/failed). Stejný anti-thundering-herd polling jako chat.
4.4 Konkrétně: app-hotline-capture → app-hotline¶
- App#1 (Android capture) → fixní folder
/sdcard/avaxhotline/(lokálně, bez sítě). - App#2 (Uploader) = exchange producer: STS upload do
users/{uid}/sync/ avaxhotline/(device sync, §3) + INSERTexchange_itemstarget=app-hotline. - app-hotline ingestion (
android-call-capture.md §12) = consumer: claim →POST /calls(call_source=android, customer_phone_hash) →PUT audio_s3_key.
→ App#2 (uploader) je generický „exchange producer" — stejný kód pro jakoukoli device→app cestu.
4.5 Connector¶
Platform connector data-exchange 1.0 (provider __platform__): provides
/v1/exchange/{next,ack,notify} + STS scope=exchange. Apps deklarují v
connector.json pár produces / consumes (kdo komu posílá). Platform při
register (per connector-registration-discipline.md) vytvoří inbox prefix +
grant. Cross-company exchange jen přes explicit grant (default per-company).
5. Chat jako instance tohoto vzoru¶
chat.md (avax-chat, JSON-per-message soubory, pending_deliveries + polling)
JE přesně tenhle vzor, kde:
- producer + consumer = users, target = DM/group prefix,
- „ingestion" = delivery (pending_deliveries → klient polluje).
Chat zůstává na dedikovaném avax-chat bucketu (cross-company, SSE-S3) — protože
je cross-company. Ostatní exchange je per-company. Sjednocení = sdílená
exchange lib (S3 write + ready-marker + DB queue + polling); chat je
nejvyzrálejší instance, kód se z něj extrahuje.
6. Provisioning (navazuje na per-app S3 gap)¶
Rozšířit backend.yml „Provision" step + create-app:
- App-data creds: dešifruj přiřazený system_app_data klíč → .env
APP_S3_ENDPOINT/BUCKET/ACCESS_KEY/SECRET_KEY.
- Exchange: app v connector.json deklaruje exchange.{produces,consumes}
→ platform vytvoří inbox prefix + grant + (consumer) ingestion worker.
- boto3 + avax-exchange client lib do template backend/requirements.txt.
- Device sync: app, co chce Android sync, deklaruje sync.folders[].
7. Bezpečnost / GDPR¶
| Hrozba | Ochrana |
|---|---|
| Únik klíčů firmy A → data firmy B | Per-company bucket; STS i static creds platí jen pro svůj bucket |
| User vidí cizí adresář | STS scoped na users/{uid}/ prefix |
| Producer píše mimo target | STS scope=exchange scoped na exchange/{target}/inbox/ |
| Permanentní klíč na telefonu | Nikdy — jen STS (1 h) + refresh_token v Keystore |
| Replay / stale token | Ceph STS jednorázové; access_token krátký |
| Ransomware/mazání | DELETE zakázán bucket policy; jen GC job |
| GDPR výmaz firmy | drop company bucket + exchange_items WHERE company_id |
| Retence | processed/ + .snapshots/ TTL; GC (garbage-collector.md) |
8. Implementační fáze¶
| Fáze | Co | Stav |
|---|---|---|
| F0 | Desktop folder sync (manifest) | ✅ sync-backup.md |
| F1 | Android device sync: mobile-auth pairing + STS sync + FileObserver upload. Pilot = app-hotline-capture App#2 (Uploader) | ⏳ |
| F2 | Generic app↔app exchange: exchange_items + data-exchange connector + /v1/exchange/* + ingestion lib |
⏳ |
| F3 | Refactor chat na sdílenou exchange lib |
později (ne nutné) |
| F4 | Global tier (cross-company shared app data) | ⏳ viz §9 |
9. Otevřené otázky (rozhodnout s Michalem)¶
- Globální vrstva (data co app sdílí VŠEM firmám — číselníky, šablony DWG,
referenční normy): dedikovaný
avax-global-{app}bucket (read-all přes presigned/CDN, write jen app backend)? Doporučuju zvlášť bucket per provider-app, read public, write M2M — mimo per-company izolaci. - Exchange umístění: per-company
companies/{cid}/exchange/(izolace, doporučeno) vs centralavax-exchange. Cross-company přenos jen přes explicit connector grant. - Notif mód: podporovat pull (default, polling — bez webhooku do appky) i push; default pull.
- Velké soubory (DWG/video): multipart upload + presigned GET; size limity
per firma (
sync-backup.mdlimity). - Sdílená
exchangelib jazyk: Python (backend) + Kotlin (Android) — dvě implementace stejného kontraktu (meta.json/ready-marker/STS).
10. Související¶
s3-architecture.md— bucket-per-firma, STS, šifrovánísync-backup.md— manifest sync, limity, GDPR identity.jsonchat.md— avax-chat, pending_deliveries, polling (vzor)s3backup.md,garbage-collector.md— retence/GCandroid-call-capture.md,android-v0.5.md— Android capture + mobile-auth pairingconnector.md,connector-registration-discipline.md—data-exchangeconnector- per-app S3 provisioning (app-data creds v
.env)