Přeskočit obsah

apps-gateway — specifikace

Status: draft Verze spec: 0.1 Aktualizováno: 2026-05-14 Související: per-app-container.md, backend-deploy.md, server-infrastructure.md

1. Cíl

apps-gateway je tenký Python/FastAPI reverse proxy, který routuje requesty /apps/<slug>/* od klientů (launcher, web) na konkrétní app kontejnery. Sedí mezi vm-gateway Nginxem (TLS termination) a app kontejnery na avaxdev / vm-api.

Co dělá: 1. Discovery aplikací z apps DB tabulky (admin DB v core-api PG) 2. Routing s prefix strip: /apps/legal/itemshttp://app-legal:8101/items 3. Per-firma channel routing (alpha/beta/stable kontejnery jako separátní upstreams) 4. Health monitoring kontejnerů — circuit breaker při opakovaných failurech 5. Pass-through Authorization headeru (JWT validuje až app middleware) 6. Strukturovaný request log + Prometheus metriky per app 7. Vystavuje admin endpointy pro hot-reload / status

Co NEDĚLÁ (záměrně): - Neukončuje TLS — vm-gateway Nginx už to dělá - Nevaliduje JWT — to dělá avax-auth middleware uvnitř každého app kontejneru. Gateway musí být rychlá a stateless; navíc validace v gateway by znamenala další round-trip k JWKS endpointu nebo statický PEM v gateway. - Negeneruje žádné tokeny - Nedělá body rewriting (jen prefix strip v path)

2. Architektura

                       ┌────────────────────────────┐
   Klient ─HTTPS─────► │   vm-gateway / Nginx       │
   (launcher,          │   api.avaxis.cz            │
    web, mobile)       │   (TLS termination)        │
                       └──┬─────────────────────┬───┘
                          │                     │
                ┌─────────┘                     └─────────┐
                │ /auth/*, /org/*,                        │ /apps/<slug>/*
                │ /catalog/*, /storage/*,                 │
                │ /admin/*                                │
                ▼                                         ▼
       ┌──────────────────┐                    ┌──────────────────────┐
       │   core-api       │                    │   apps-gateway       │
       │   (FastAPI)      │ ──── DB pool ────► │   (FastAPI :8100)    │
       │   :8000          │                    │   - discovery        │
       └────────┬─────────┘                    │   - routing          │
                │                              │   - health monitor   │
                │                              │   - metrics/logs     │
                ▼                              └─────────┬────────────┘
       ┌──────────────────┐                              │
       │  PostgreSQL      │ ◄── read-only ───────────────┘ (apps, app_deployments,
       │  + PgBouncer     │     (cached, refresh 60s)       company_channel_assignments)
       └──────────────────┘                              │
                                  ┌──────────────────────┼─────────────────────┐
                                  ▼                      ▼                     ▼
                          ┌──────────────┐      ┌──────────────┐      ┌──────────────┐
                          │ app-legal    │      │ app-mzdy     │      │ app-xxx      │
                          │ :8101        │      │ :8102        │      │ :81NN        │
                          │ (Docker net) │      │              │      │              │
                          └──────────────┘      └──────────────┘      └──────────────┘

Síťová topologie (avaxdev): - Apps-gateway běží jako systemd unit nebo Docker kontejner, port :8100 - App kontejnery běží v Docker bridge networku avax-apps na portech 8101–8199 - Gateway připojena do stejné networku → mluví s nimi přes container hostname (app-legal:8101) NEBO host.docker.internal:81xx - Nginx → apps-gateway: TCP 8100 na localhost (loopback only)

Síťová topologie (production vm-api): - Stejně, ale apps-gateway běží spolu s containers na vm-api - 2× replika gateway za internal HAProxy / Nginx load balancer pro HA

3. Discovery — DB-driven

Apps-gateway čte konfiguraci z apps tabulky core-api DB (read-only). Refresh každých 60 sekund + push-trigger přes Redis pubsub při create_app / update_app / delete_app.

Rozšíření apps modelu (nová migrace)

Přidat sloupce:

# backend/app/models/catalog.py — App class
gateway_enabled:      Mapped[bool]     = mapped_column(Boolean, default=False, nullable=False)
container_host:       Mapped[str]      = mapped_column(String(255), default="localhost")  # nebo container name
container_port:       Mapped[int|None] = mapped_column(Integer)        # 8101–8199
container_image:      Mapped[str|None] = mapped_column(String(500))    # git.avaxis.cz/avax-apps/legal:1.0.3
health_path:          Mapped[str]      = mapped_column(String(100), default="/health")
health_status:        Mapped[str]      = mapped_column(String(20), default="unknown")
                                       # unknown | ok | degraded | down
last_health_check_at: Mapped[datetime|None] = mapped_column(DateTime(timezone=True))
last_health_error:    Mapped[str|None] = mapped_column(Text)

gateway_enabled=True = app je registrovaná v gateway. Default False aby existující module-mount apps nezačaly náhle 404-ovat přes gateway.

Per-channel deployments (volitelné v MVP)

Pro per-firma routing (alpha/beta/stable) by ideálně každá channel měla vlastní kontejner s vlastním portem. Návrh: nová tabulka app_deployments:

class AppDeployment(Base):
    __tablename__ = "app_deployments"
    __table_args__ = (UniqueConstraint("app_id", "channel"),)

    id:                   Mapped[uuid.UUID]
    app_id:               Mapped[uuid.UUID] = mapped_column(ForeignKey("apps.id"))
    channel:              Mapped[str]       = mapped_column(String(20))  # alpha|beta|stable
    container_host:       Mapped[str]
    container_port:       Mapped[int]
    container_image:      Mapped[str]
    health_status:        Mapped[str]
    last_health_check_at: Mapped[datetime|None]
    last_health_error:    Mapped[str|None]
    deployed_at:          Mapped[datetime]
    deployed_by_user_id:  Mapped[uuid.UUID|None]

V MVP gateway podporuje pouze stable (1:1 s apps). Per-channel deploys přidat v Phase 2 — viz Roadmap.

Cache invalidation

  • Polling refresh — každých 60s gateway re-loaduje apps WHERE gateway_enabled = TRUE
  • Push invalidation — core-api publishuje Redis channel apps.changed payload {slug, action: "create|update|delete"}, gateway reaguje okamžitě (<1s propagace)
  • Manual refreshPOST /admin/refresh endpoint v gateway (jen ze loopbacku)

4. Routing pravidla

Path rewriting

Request:   GET /apps/legal/items/123?foo=bar
                ────┬──── ─────┬──────
                  slug      sub-path

Upstream:  GET http://app-legal:8101/items/123?foo=bar
  • Prefix /apps/<slug> se odstraňuje
  • Query string a body se předávají beze změny
  • Trailing slash zachován: /apps/legal//
  • Pokud <slug> neexistuje v cache → 404 {"detail": "Aplikace neexistuje", "slug": "..."}
  • Pokud health_status == "down"503 {"detail": "Aplikace dočasně nedostupná"}

Headery

Forwardované 1:1: - Authorization: Bearer <jwt> — gateway nevalidační - Accept, Content-Type, Content-Length, User-Agent - X-Request-ID — pokud klient pošle, jinak gateway vygeneruje UUID4

Přidávané gateway: - X-Forwarded-For — IP klienta (přes Nginx proxy chain) - X-Avax-Slug: legal — explicit slug pro app interní log - X-Avax-Channel: stable — pokud byl použit per-channel deploy - X-Avax-Gateway-Version: 0.1.0

Odstraňované: - X-Forwarded-Host (gateway forwarduje vlastní hostname) - Hop-by-hop headery (Connection, Upgrade, …) — RFC 7230 § 6.1

Body & metody

  • Všechny HTTP metody: GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD
  • Streaming request body (chunked) → streaming proxy do upstreamu (httpx.AsyncClient.stream)
  • Streaming response (chunked, SSE, WebSocket) → bidirectional proxy

WebSocket (Phase 2): apps-gateway zatím NE-WS. App který potřebuje WS si může poslat WS přímo přes vm-gateway Nginx (/apps-ws/<slug>/* na separátním backendu) — to je future work.

5. Per-firma channel routing

Cíl: firma A je v beta, firma B v stable → každá dostane jiný kontejner stejné app.

Tok requestu

1. Klient → /apps/legal/items   Authorization: Bearer <jwt>
2. Nginx → apps-gateway (port 8100)
3. apps-gateway:
   a) extract slug = "legal"
   b) decode JWT (bez validace signature — gateway nezná secret!)
      → claims.company_id
   c) lookup channel:
      SELECT channel FROM company_channel_assignments
      WHERE company_id = $1 AND app_id = $app_legal_id
      (cached 5min v gateway, default = "stable")
   d) lookup deployment:
      SELECT container_host, container_port FROM app_deployments
      WHERE app_id = $app_legal_id AND channel = $channel
      (default = stable pokud channel deployment neexistuje)
   e) forward na vybraný upstream

Bezpečnost: gateway čte company_id z claims bez verifikace signature. To je OK protože: - Gateway forwarduje request app kontejneru, který ZNOVU validuje JWT s avax-auth middleware - I kdyby útočník podstrčil forged JWT s jiným company_id, app middleware ho odmítne - Gateway-level channel lookup je jen routing hint, ne security boundary

Fallback při chybějícím JWT: /health/* paths jsou veřejné, ostatní bez JWT → routing na stable.

MVP cesta (per-channel deploy bez tabulky app_deployments)

V MVP gateway čte jen apps.container_host/port (vždy stable). Per-channel routing přidat v Phase 2 po stable funkcionalitě.

6. Health monitoring & circuit breaker

Health checks

Každých 10 sekund gateway pingne každou registrovanou app:

GET http://<container_host>:<container_port><health_path>
Timeout: 5s

Stav transitions:

Trigger Z → do Akce
200 OK * → ok Requesty propouštět
200 OK s degraded checks * → degraded Requesty propouštět + alert log
503 / timeout / connection error ok|degradeddegraded 1× retry za 5s
3 po sobě fail * → down 503 na klientské requesty, ping interval 30s
200 OK downok Obnovit obsluhu, reset counteru

health_status + last_health_check_at + last_health_error se zapisují do apps tabulky každých 10s (jednoduché UPDATE).

Circuit breaker pattern

Při down gateway nesendí requesty na upstream (jinak by každý request platil 5s timeout cenu). Místo toho rovnou vrátí:

HTTP/1.1 503 Service Unavailable
Content-Type: application/json
Retry-After: 30

{
  "detail": "Aplikace 'legal' dočasně nedostupná",
  "slug": "legal",
  "status": "down",
  "last_error": "Connection refused",
  "retry_after_s": 30
}

Public status endpoint

GET /apps/status (na gateway, bez auth) → seznam všech apps + status. Pro launcher tray widget („avax-legal je momentálně mimo provoz"):

{
  "apps": [
    {"slug": "legal", "status": "ok",       "last_check": "...", "version": "1.0.3"},
    {"slug": "mzdy",  "status": "degraded", "last_check": "...", "last_error": "DB lag 3s"}
  ]
}

7. Service tokens (M2M) — OAuth2 client_credentials

Implementováno 2026-05-28 (commits da9788d + 9ce3880 v core-api). Aktualizace tohoto designu z původního service-token návrhu — nahrazeno standardním OAuth2 client_credentials grant pro snazší interop s vendor service ekosystémem (CI runners, cron jobs, external SaaS partners).

Use cases

Některé background procesy musí volat AI Helper / další apps bez user kontextu: - Vendor cron noční reindex legal-precedents corpus - Batch embedding 1000 dokumentů přes Celery - External SaaS partner volá /v1/ai/chat/complete jménem koncového klienta - Per-app backend volá jiný per-app backend (např. mzdy volá legal pro precedent lookup)

Issuance: POST /admin/m2m-clients (super_admin only)

Super_admin vystaví credentials přes core-api admin endpoint:

POST /admin/m2m-clients
Authorization: Bearer <super_admin_jwt>
Content-Type: application/json

{
  "owner_vendor_id": "<vendor company UUID>",
  "allowed_app_id":  "<app UUID>",
  "name":            "legal-cron-batch",
  "description":     "...",
  "allowed_scopes":  ["ai-helper.chat", "ai-helper.rag.query"]
}

→ 201 {
  "id":            "<UUID>",
  "client_id":     "avax_m2m_<32 hex>",
  "client_secret": "avax_secret_<48 url-safe>",   ← PLAINTEXT JEDNOU
  ...
}

Bezpečnost: - client_id formát: avax_m2m_<32 hex> (public identifier) - client_secret formát: avax_secret_<48 url-safe> (~64 chars) - DB ukládá jen bcrypt hash secretu — plaintext jen v response z create - Vendor uloží do Bitwarden / CI env vars / docker secret — NIKDY do conversation logu, gitu, Slack - Pokud vendor secret ztratí: POST /admin/m2m-clients/{id}/revoke → vystavit nové

Token issuance: POST /auth/token

POST /auth/token
Content-Type: application/json

{
  "grant_type":    "client_credentials",
  "client_id":     "avax_m2m_...",
  "client_secret": "avax_secret_...",
  "scope":         "ai-helper.chat ai-helper.rag.query"   ← optional subset
}

→ 200 {
  "access_token": "<RS256 JWT, kid v headeru>",
  "token_type":   "Bearer",
  "expires_in":   900,                              ← 15 min default
  "scope":        "ai-helper.chat ai-helper.rag.query"
}

JWT claims

Podepsáno RS256 stejnou platform private key jako user JWT (sdílený JWKS endpoint /auth/.well-known/jwks.json → per-app AVAX_JWKS_URL). Avax-auth middleware ho validuje stejně jako user JWT:

{
  "sub":          "<vendor_uuid>",
  "company_id":   "<vendor_uuid>",       // M2M caller = vendor company
  "app_id":       "<allowed_app_uuid>",  // ACL — apps mohou filtrovat
  "vendor_id":    "<vendor_uuid>",
  "client_id":    "avax_m2m_<32 hex>",
  "scopes":       ["ai-helper.chat", ...],
  "is_m2m":       true,                  // distinguishuje od user JWT
  "system_role":  "m2m",                 // NE 'user' / 'super_admin'
  "tfa_verified": false,
  "type":         "access",
  "exp":          <unix ts>
}

Důležitá designová rozhodnutí: - sub = vendor_uuid (NE vendor:<uuid> prefix — avax-auth validator očekává parseable UUID v sub) - company_id = vendor_uuid (vendor JE caller's company sémantika) - system_role="m2m" umožňuje app code filtrovat (if user.system_role == "m2m": ... pro audit log / quota separation)

Scope filtering

Apps validují že caller má scope na danou operaci:

# V app-ai-helper endpoint handleru:
@router.post("/v1/chat/stream")
async def chat_stream(req: ChatRequest, user: AvaxUser = Depends(...)):
    if user.system_role == "m2m":
        scopes = user.claims.get("scopes", [])
        if "ai-helper.chat" not in scopes:
            raise HTTPException(403, "scope ai-helper.chat required")
    # ... handler

Předdefinované scopes (per ai-helper.md): - ai-helper.chat/v1/chat/* - ai-helper.embed/v1/embed* - ai-helper.rag.query/v1/rag/query - ai-helper.rag.write/v1/rag/corpora + grants - ai-helper.image/v1/image/* - ai-helper.generic_call/v1/call/{cap_id} (libovolný capability)

V gateway

Gateway si žádný service token nedrží — vendor service získá token přes /auth/token (core-api), pak ho posílá v Authorization: Bearer na /apps/<slug>/v1/*. Gateway proxuje bez ohledu na auth type (user/M2M). Validuje až per-app middleware.

Plus: gateway by mohla v budoucnu rate-limitovat per client_id claim (současné rate-limit per IP) — sekce 10 roadmap.

Admin endpointy

GET    /admin/m2m-clients              — list (filter vendor_id / app_id)
GET    /admin/m2m-clients/{id}         — detail (bez secret)
POST   /admin/m2m-clients/{id}/revoke  — soft revoke (existing tokens
                                          expire do 15 min naturally)
DELETE /admin/m2m-clients/{id}         — hard delete (POZOR audit trail)

SDK použití (M2M flow)

Vendor service v Pythonu:

import httpx, os
from avaxis_sdk import AvaxApp

# Získat M2M JWT
resp = httpx.post("https://api.avaxis.cz/auth/token", json={
    "grant_type":    "client_credentials",
    "client_id":     os.environ["AVAX_M2M_CLIENT_ID"],
    "client_secret": os.environ["AVAX_M2M_CLIENT_SECRET"],
    "scope":         "ai-helper.chat ai-helper.rag.query",
})
m2m_token = resp.json()["access_token"]

# Nastav do SDK + použij
app = AvaxApp(slug="moje-cron")
app.access_token = m2m_token
text = await app.ai.chat_complete(prompt="...")

Per-app backend M2M flow podobně — httpx.post na core-api, výsledný token použít v dalších volání.

8. Failure modes

Selhání Co gateway dělá Co klient vidí
Slug neexistuje 404 {"detail": "Aplikace neexistuje", "slug": "..."}
Slug existuje, health = down 503 (circuit broken) {"detail": "Aplikace dočasně nedostupná", "retry_after_s": 30}
Upstream timeout (10s) 504 Gateway Timeout {"detail": "App neodpovídá", "timeout_s": 10}
Upstream 5xx proxy 5xx beze změny upstream tělo + status
Upstream připojení selhalo (connection refused) 502 Bad Gateway {"detail": "App nedosažitelná"}
DB pro discovery nedostupná Použij in-memory cache (last known), log error klient transparentně dostane response
Redis pubsub down Žádný error — fallback na polling refresh

Request body při retry: gateway nereetri request body při upstream selhání (idempotence není garantována; PUT/POST mohou mít side effects). Klient může retry sám.

9. Observability

Strukturovaný log (JSON do stdout)

{
  "ts": "2026-05-14T17:30:00Z",
  "level": "INFO",
  "logger": "apps_gateway",
  "request_id": "uuid4",
  "method": "POST",
  "path": "/apps/legal/items",
  "slug": "legal",
  "channel": "stable",
  "upstream": "app-legal:8101",
  "status": 200,
  "duration_ms": 42,
  "user_id": "<uuid>",          // z JWT claims, ne validovaný
  "company_id": "<uuid>",
  "bytes_in": 1234,
  "bytes_out": 5678
}

Agregace přes vm-monitor (Loki) — již existující stack.

Prometheus metriky

GET /metrics na gateway (port 8100, no auth, ale binded na loopback):

# Counters
apps_gateway_requests_total{slug, channel, method, status}
apps_gateway_upstream_errors_total{slug, channel, error_type}

# Histograms
apps_gateway_request_duration_seconds{slug, channel}    # response time
apps_gateway_upstream_duration_seconds{slug, channel}   # bez gateway overheadu

# Gauges
apps_gateway_app_health{slug, channel}    # 0=down, 1=degraded, 2=ok
apps_gateway_active_requests{slug}
apps_gateway_cached_apps_count

vm-monitor (Prometheus) scrape každých 15s.

Tracing (Phase 3+)

OpenTelemetry → Tempo. V MVP nesoučástí — přidat až budou apps multi-hop (app → core-api → DB).

10. Rate limiting (Phase 2)

V MVP bez rate limitingu. V Phase 2:

  • Per company × per app: 100 req/min default, override v apps.rate_limit_per_company_per_min
  • Per user × per app: 30 req/min default
  • Globální per app: 1000 req/min per kontejner replika (DoS protection)

Implementace: in-memory token bucket per (company_id, slug) klíč. Pro multi-replika gateway: Redis-based bucket (redis-cell extenze nebo Lua skript).

Při překročení: 429 Too Many Requests + Retry-After: <s>.

11. Deploy

Avaxdev (dev)

Systemd unit avax-apps-gateway.service:

[Unit]
Description=AVAX apps-gateway (FastAPI reverse proxy)
After=network.target postgresql.service redis-server.service avax-backend-bare.service
Wants=avax-backend-bare.service

[Service]
Type=exec
User=avax
WorkingDirectory=/home/avax/avax-platform/apps-gateway
Environment="AVAX_GATEWAY_DB_URL=postgresql+asyncpg://..."
Environment="AVAX_GATEWAY_REDIS_URL=redis://localhost:6379/3"
Environment="AVAX_GATEWAY_PORT=8100"
Environment="AVAX_GATEWAY_LOG_LEVEL=INFO"
ExecStart=/home/avax/avax-platform/.venv/bin/uvicorn apps_gateway.main:app \
          --host 127.0.0.1 --port 8100 \
          --proxy-headers --forwarded-allow-ips=127.0.0.1
Restart=on-failure
RestartSec=3s

[Install]
WantedBy=multi-user.target

Code lives in apps-gateway/ v hlavním repu (peer s backend/).

Production vm-api (Phase 5)

Docker compose service v vm-api/docker-compose.yml:

services:
  apps-gateway:
    image: git.avaxis.cz/avaxis/apps-gateway:0.1.0
    networks: [avax-apps]
    environment:
      AVAX_GATEWAY_DB_URL: ${DB_URL}
      AVAX_GATEWAY_REDIS_URL: redis://redis:6379/3
    ports:
      - "127.0.0.1:8100:8100"
    deploy:
      replicas: 2

Nginx config

# vm-gateway nginx — /etc/nginx/sites-available/api.avaxis.cz
location /apps/ {
    proxy_pass http://127.0.0.1:8100;
    proxy_http_version 1.1;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_read_timeout 60s;
    proxy_buffering off;          # streaming
}

12. Konfigurace (env vars)

Variable Default Účel
AVAX_GATEWAY_PORT 8100 Listen port
AVAX_GATEWAY_DB_URL (none) PG connection pro discovery (read-only user doporučen)
AVAX_GATEWAY_REDIS_URL redis://localhost:6379/3 Pubsub channel apps.changed
AVAX_GATEWAY_REFRESH_S 60 Polling interval pro DB refresh
AVAX_GATEWAY_HEALTH_INTERVAL_S 10 Health check tick
AVAX_GATEWAY_HEALTH_TIMEOUT_S 5 Health check timeout
AVAX_GATEWAY_UPSTREAM_TIMEOUT_S 60 Upstream request timeout
AVAX_GATEWAY_LOG_LEVEL INFO uvicorn log level
AVAX_GATEWAY_METRICS_ENABLED true Vystavit /metrics

13. Bezpečnost

  • Network isolation: gateway binded jen na 127.0.0.1:8100. Veřejně přístupná POUZE přes Nginx s TLS.
  • JWT: gateway nevalidační (perf reason). App middleware validuje. Forged JWT s vyšším system_role nepomůže — app validuje signature.
  • DB user pro discovery: read-only role avax_gateway s SELECT jen na apps, app_deployments, company_channel_assignments.
  • Service token secrets: nikdy v gateway logu. App secret leak → admin musí rotovat přes admin UI.
  • Audit log: každý request s user_id (z claims) do strukturovaného logu. Slug s system_role=service označen jako M2M call.
  • DoS: rate limiting per company (Phase 2), upstream connection pool s max-conn limitem per app.

14. Otevřené otázky

  • WebSockets — gateway WS proxy v MVP, nebo přes Nginx s vlastním backendem? Začínáme bez WS v gateway, doladit s prvním WS-using app.
  • Streaming uploads (multipart) — limit velikosti? Default 100 MB (Nginx client_max_body_size) + gateway nestreamuje do disku, jen pipe.
  • Per-channel deployments — DB schema app_deployments zavést v MVP, nebo až v Phase 2? Pro MVP stačí 1:1 v apps tabulce; v Phase 2 přidat tabulku.
  • JWT decode bez validace v gateway — extrahovat company_id z claims. Alternativně předat plain a app vrátí company_id v X-Avax-Company-ID response headeru, gateway si to nakešuje pro analytics. (Preferujeme prvotní variantu — jednoduší.)
  • Service token scope — JWT bez company_id znamená "globální". Má app endpoint požadovat company_id jako URL parameter pro service-role calls, nebo z headeru X-Avax-Target-Company?
  • CI deploy app — kdo registruje container_port? Vendor v app.yml repa? CI auto-allocate z range 8101–8199?
  • Cross-app komunikace — povolit app-legal volat app-mzdy přes gateway (http://apps-gateway:8100/apps/mzdy/...)? Bezpečnostně to není problém (každá app validuje JWT/service token), ale discouraged — preferovat přes core-api.
  • Graceful shutdown — při SIGTERM dokončit in-flight requests (uvicorn handles, ale s explicit timeout 30s).
  • Multi-replika sync — pokud běží 2 instance gateway (HA), oba dělají health check (zbytečné × 2). Lze koordinovat přes Redis lock (jen 1 dělá písemnou kontrolu, ostatní jen čtou výsledek z DB), ale jednoduší: oba ping, last-write-wins na DB.

15. Roadmap

Fáze Co Cílový termín
Phase 1 — Spec Tento dokument + review 2026-05-14 ✓
Phase 2a — Skeleton apps-gateway/ adresář, FastAPI app, env config, systemd unit TBD
Phase 2b — DB rozšíření Migrace 020: apps.gateway_enabled/container_host/port/... + read-only role avax_gateway TBD
Phase 2c — Discovery DB read + Redis pubsub apps.changed TBD
Phase 2d — Routing Prefix strip, header passthrough, httpx upstream TBD
Phase 2e — Health /health polling + circuit breaker + apps.health_status writeback TBD
Phase 2f — Observability JSON logy + Prometheus /metrics TBD
Phase 3 — Pilot avax-legal Skutečná migrace avax_legal modulu do kontejneru za gateway TBD
Phase 4 — Per-channel Tabulka app_deployments, channel routing z company_channel_assignments TBD
Phase 5 — Service tokens POST /auth/service-token, avax-auth.require_service_role() TBD
Phase 6 — Rate limit Per company × per app token bucket TBD
Phase 7 — Production HA 2× replika + Nginx LB na vm-api TBD

16. Akceptační kritéria — Phase 2 MVP

  • apps-gateway startuje na :8100 (uvicorn z systemd unit)
  • Při startu načte apps WHERE gateway_enabled=TRUE z DB
  • GET /apps/<slug>/health proxy-uje na http://<container_host>:<container_port>/health
  • Prefix strip funguje (/apps/legal/items → upstream /items)
  • Pass-through Authorization headeru
  • Health monitor každých 10s, apps.health_status aktualizovaný
  • Circuit breaker: 3 fail → down → klient dostane 503 bez čekání na timeout
  • GET /apps/status veřejný endpoint vrátí JSON se stavem všech apps
  • GET /metrics exportuje Prometheus formátu
  • Strukturované JSON logy do stdout
  • Smoke test: nasimulovat 1 app (Python http.server) registrovaný v DB, curl /apps/test/foo dorazí, restart app → down → opět up

17. Související