# Contract: satellite-provider tile serving **Component**: satellite-provider **Producer task**: TBD — separate AZAION ticket on `satellite-provider` workspace (user-filed) **Consumer tasks**: AZ-498 — `_docs/02_tasks/todo/AZ-498_satellite_tile_swap.md` (suite/ui, cycle 2, epic AZ-497) **Version**: 1.0.0 **Status**: draft **Last Updated**: 2026-05-12 ## Purpose Describe the slippy-tile HTTP interface that the suite UI consumes to render satellite imagery in `FlightMap` / `MiniMap`. Replaces the prior external-tile dependencies (OpenStreetMap, Esri ArcGIS World Imagery). The endpoint is served by `SatelliteProvider.Api` and backed by an on-disk + Google-Maps download cache. Frozen post-migration: SPA authentication for this endpoint MUST be **cookie-based** (JWT delivered via `HttpOnly; Secure; SameSite=Lax` cookie on the same origin) because Leaflet's `` issues plain `` requests and cannot attach `Authorization: Bearer …` headers. ## Shape ### HTTP / RPC endpoints | Method | Path | Request body | Response | Status codes | |--------|-------------------------------|--------------|-------------------|---------------------| | `GET` | `/tiles/{z}/{x}/{y}` | — | image bytes | 200, 401, 404, 503 | **Path parameters** | Name | Type | Required | Range / Constraint | |------|---------|----------|--------------------------------------------------------| | `z` | `int` | yes | `0 ≤ z ≤ 20` (slippy-tile zoom) | | `x` | `int` | yes | `0 ≤ x < 2^z` (slippy-tile column) | | `y` | `int` | yes | `0 ≤ y < 2^z` (slippy-tile row, TMS-y convention NO) | Coordinates follow the Google Maps / OSM XYZ tiling scheme (NOT the inverted TMS y-axis). Out-of-range coordinates SHOULD return 404. **Response headers (on 200)** | Header | Value | |------------------|---------------------------------------------------------------| | `Content-Type` | `image/jpeg` (image bytes from the `TileService`) | | `Cache-Control` | `public, max-age=N` where N is set by `TileService` | | `ETag` | strong ETag tied to the cached tile's content hash | **Authentication** - **Required**: yes (the endpoint is NOT public). - **Mechanism (post-migration)**: cookie-based JWT. - Cookie name: `satellite_auth` (TBD — defined by producer task). - Attributes: `HttpOnly; Secure; SameSite=Lax` in production; `SameSite=Lax` permitted over `http://localhost` for dev only. - **Cross-origin behavior**: same-origin only. The SPA reaches this endpoint via the suite ingress (nginx) on the SPA's origin; cross-origin direct calls from `http://localhost:5173 → http://localhost:5100` will NOT carry the cookie and will receive 401 in dev unless the developer disables auth locally. **Status codes** | Code | Meaning | |------|-------------------------------------------------------------------| | 200 | Cached or freshly downloaded tile; body = image bytes | | 304 | (Optional) ETag match — body empty. UI MUST tolerate either 200 or 304. | | 401 | Missing/invalid cookie — UI MUST treat as "user signed out" | | 404 | Tile coordinates out of range OR upstream had no tile | | 503 | Upstream (Google Maps) unavailable; UI MUST render placeholder | ## Invariants - The endpoint URL pattern is `/tiles/{z}/{x}/{y}` exactly — never `/tiles/{z}/{y}/{x}` (Esri-style) nor `/api/satellite/tiles/{z}/{x}/{y}`. This invariant survives refactors and is asserted by both producer's integration tests and consumer's blackbox tests. - Image format is JPEG (Content-Type `image/jpeg`). Switching to PNG/WEBP is a major-version change. - The endpoint MUST honor `Cache-Control` and `ETag` headers on every 200; clients rely on them to avoid re-fetching unchanged tiles during pan/zoom. - Authentication failure MUST return 401, not 200 with an HTML body — Leaflet would otherwise display a broken-image placeholder silently. ## Non-Goals - Not covered: tile vector formats (`.pbf` / Mapbox Vector Tiles). This contract is raster-only. - Not covered: tile prewarming. Pre-warm uses the separate `POST /api/satellite/request` endpoint (different contract, not consumed by the UI's `FlightMap`). - Not covered: MGRS tile retrieval (returns 501 today; out of UI scope). ## Versioning Rules - **Breaking** (major bump): change the path template, change the path-parameter semantics (e.g., TMS-y), change `Content-Type`, remove a status code from the set above, change the auth mechanism away from cookies. - **Non-breaking** (minor bump): add a new optional query parameter, broaden the zoom range, add a new status code in the 4xx/5xx space that consumers can tolerate. ## Test Cases | Case | Input | Expected | Notes | |----------------------------|----------------------------------------|-----------------------------------------------------------|----------------------------------| | valid-tile | `GET /tiles/15/9876/5432` w/ cookie | 200 + JPEG bytes + `Cache-Control` + `ETag` | producer + consumer cover | | missing-cookie | `GET /tiles/15/9876/5432` w/o cookie | 401 | consumer must NOT retry | | out-of-range-coord | `GET /tiles/3/8/0` (x ≥ 2^z) | 404 | consumer renders placeholder | | etag-match | `GET /tiles/15/9876/5432` + `If-None-Match` | 304 OR 200 (server-policy dependent) | consumer tolerates both | | upstream-503 | upstream Google Maps down | 503 | consumer renders placeholder | | zoom-extreme | `GET /tiles/20/x/y` valid coords | 200 (or 404 if not cached and no on-demand) | consumer caps zoom at 20 | ## Change Log | Version | Date | Change | Author | |---------|------------|------------------------------------------------------------------------------|--------| | 1.0.0 | 2026-05-12 | Initial draft; freezes the post-migration shape (cookie auth, XYZ scheme). | autodev (cycle 2 — suite/ui) |