# 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 `<TileLayer>` issues plain `<img>` 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) |
