openapi: 3.1.0
info:
  title: Relayer API
  version: "1.0"
  description: |
    The release control plane. Decisions, not bytes.

    Two surfaces:
    - **Management API** (`/api/v1/*`): API-key authenticated. Publish
      releases, discover apps, and drive push-mode fleets.
    - **Device endpoints** (`/u`, `/e`, `/feed`): public, called by devices
      or updaters. No key; devices identify with an anonymous UUID.

    Rate limits: 300 requests/minute per key on the management API,
    120/minute per device on device endpoints. `429` responses include a
    `Retry-After` header.
servers:
  - url: /
    description: This deployment
security: []
tags:
  - name: Releases
    description: Publish and list releases. Every publish is audited under the key that performed it.
  - name: Apps
    description: Discover app refs and channels, handy for CI.
  - name: Push mode
    description: |
      For fleets where an orchestrator pushes updates and devices never
      call `GET /u/` themselves. Report state on devices' behalf and plan
      whole update waves in one call. See the push-mode guide.
  - name: Device endpoints
    description: Public endpoints spoken by devices and updaters. No authentication.

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: "rl_… API key"
      description: Mint keys from the dashboard (API keys). Shown once, stored hashed.
  schemas:
    Error:
      type: object
      required: [error]
      properties:
        error:
          type: string
          examples: ["Unknown app"]
    Artifact:
      type: object
      required: [platform]
      properties:
        platform:
          type: string
          description: darwin | windows | linux (or your own identifier)
          examples: [darwin]
        arch:
          type: string
          description: aarch64, x86_64, universal, or omitted for platform-wide artifacts
          examples: [aarch64]
        url:
          type: string
          format: uri
          description: Where the artifact lives (your storage, not Relayer's)
        sha512:
          type: string
          description: Checksum, passed through to updaters that verify it
        size:
          type: integer
          description: Bytes
        signature:
          type: string
          description: Code-signing signature (e.g. Tauri minisign), passed through verbatim
    Release:
      type: object
      properties:
        id:
          type: string
        app:
          type: string
          description: App ref
        channel:
          type: string
        version:
          type: string
        notes:
          type: string
          description: Markdown release notes
        status:
          type: string
          enum: [published, paused, rolled_back]
        rolloutPct:
          type: integer
          minimum: 0
          maximum: 100
        createdAt:
          type: string
          format: date-time
        artifacts:
          type: array
          items:
            $ref: "#/components/schemas/Artifact"
    UpdateManifest:
      type: object
      description: What a device should do. Also Tauri's dynamic-updater contract.
      required: [version, notes, pub_date, updateMode]
      properties:
        version:
          type: string
        notes:
          type: string
        pub_date:
          type: string
          format: date-time
        url:
          type: string
          format: uri
        sha512:
          type: string
        signature:
          type: string
        size:
          type: integer
        updateMode:
          type: string
          enum: [optional, recommended, required]
          description: required when the device is below the channel's minimum supported version
        minimumSupportedVersion:
          type: string
    DeviceState:
      type: object
      required: [deviceId, version, platform]
      properties:
        deviceId:
          type: string
          maxLength: 128
          description: Your stable identifier for the device
        version:
          type: string
          description: Version the device is observed running
        platform:
          type: string
        arch:
          type: string
        channel:
          type: string
          default: stable
        labels:
          type: object
          maxProperties: 16
          additionalProperties:
            type: string
            maxLength: 256
          description: |
            Opaque key/values (customerRef, siteRef, routerName, …).
            Stored and echoed, never interpreted. A report without labels
            never clears previously attached labels.
    Decision:
      type: object
      required: [deviceId, update]
      properties:
        deviceId:
          type: string
        update:
          oneOf:
            - $ref: "#/components/schemas/UpdateManifest"
            - type: "null"
          description: null = up to date, outside the rollout cohort, or no artifact for the platform

paths:
  /api/v1/releases:
    post:
      operationId: createRelease
      tags: [Releases]
      security:
        - bearerAuth: []
      summary: Publish a release
      description: |
        Creates an immutable release on a channel. Artifacts point at your
        existing storage. Duplicate app+channel+version returns `409`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [app, channel, version]
              properties:
                app:
                  type: string
                  description: App ref (shown on the app page) or app id
                channel:
                  type: string
                  description: Must exist on the app
                version:
                  type: string
                  description: Semver-ish, unique per app+channel
                notes:
                  type: string
                  description: Markdown release notes
                artifacts:
                  type: array
                  items:
                    $ref: "#/components/schemas/Artifact"
            example:
              app: qsxsicotpbmtf8ag
              channel: stable
              version: 1.4.2
              notes: "## 1.4.2\n\n- Fixes the sync-engine memory leak"
              artifacts:
                - platform: darwin
                  arch: aarch64
                  url: https://github.com/you/app/releases/download/v1.4.2/app-darwin-arm64.dmg
                  sha512: "…"
      responses:
        "201":
          description: Release created
          content:
            application/json:
              schema:
                type: object
                properties:
                  release:
                    $ref: "#/components/schemas/Release"
        "400":
          description: Missing or invalid fields
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Invalid or revoked API key
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Unknown app or channel
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "409":
          description: Version already exists on that app+channel
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "429":
          description: Rate limit exceeded (Retry-After header included)
    get:
      operationId: listReleases
      tags: [Releases]
      security:
        - bearerAuth: []
      summary: List releases
      description: Newest first, paginated.
      parameters:
        - name: app
          in: query
          schema:
            type: string
          description: App ref
        - name: channel
          in: query
          schema:
            type: string
        - name: page
          in: query
          schema:
            type: integer
            default: 1
        - name: per_page
          in: query
          schema:
            type: integer
            maximum: 100
      responses:
        "200":
          description: Paginated releases
          content:
            application/json:
              schema:
                type: object
                properties:
                  releases:
                    type: array
                    items:
                      $ref: "#/components/schemas/Release"
                  page:
                    type: integer
                  pageCount:
                    type: integer
                  total:
                    type: integer
        "401":
          description: Invalid or revoked API key
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /api/v1/apps:
    get:
      operationId: listApps
      tags: [Apps]
      security:
        - bearerAuth: []
      summary: List apps
      description: Your organization's apps with refs and channels.
      responses:
        "200":
          description: Apps for the key's organization
          content:
            application/json:
              schema:
                type: object
                properties:
                  apps:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: string
                        name:
                          type: string
                        ref:
                          type: string
                        channels:
                          type: array
                          items:
                            type: string
        "401":
          description: Invalid or revoked API key
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /api/v1/fleet/report:
    post:
      operationId: reportFleet
      tags: [Push mode]
      security:
        - bearerAuth: []
      summary: Report fleet state
      description: |
        A heartbeat on devices' behalf. Devices are upserted on
        `(app, deviceId)` and appear in the fleet dashboard, adoption
        charts and live feed exactly as if they had checked in themselves.
        Up to 500 devices per call.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [app, devices]
              properties:
                app:
                  type: string
                  description: App ref or id
                devices:
                  type: array
                  minItems: 1
                  maxItems: 500
                  items:
                    $ref: "#/components/schemas/DeviceState"
            example:
              app: qsxsicotpbmtf8ag
              devices:
                - deviceId: router-0042
                  version: 1.4.0
                  platform: linux
                  arch: aarch64
                  labels:
                    customerRef: acme-42
                    siteRef: blr-dc1
      responses:
        "200":
          description: Report accepted
          content:
            application/json:
              schema:
                type: object
                properties:
                  app:
                    type: string
                    description: App ref
                  received:
                    type: integer
        "400":
          description: Validation failed (message names the field)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Invalid or revoked API key
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Unknown app (or app not in your organization)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /api/v1/fleet/decisions:
    post:
      operationId: planFleetDecisions
      tags: [Push mode]
      security:
        - bearerAuth: []
      summary: Plan an update wave
      description: |
        One decision per device, using the exact same rollout logic as
        `GET /u/`: version comparison, sticky staged-rollout cohorts with
        fall-through, artifact matching and channel policy. Up to 500
        devices per call, one rate-limit hit.

        `dryRun: true` plans without writing telemetry (no device upserts,
        no check-ins).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [app, devices]
              properties:
                app:
                  type: string
                channel:
                  type: string
                  default: stable
                dryRun:
                  type: boolean
                  default: false
                devices:
                  type: array
                  minItems: 1
                  maxItems: 500
                  items:
                    type: object
                    required: [deviceId, version, platform]
                    properties:
                      deviceId:
                        type: string
                      version:
                        type: string
                      platform:
                        type: string
                      arch:
                        type: string
            example:
              app: qsxsicotpbmtf8ag
              channel: stable
              devices:
                - deviceId: router-0042
                  version: 1.4.0
                  platform: linux
                  arch: aarch64
      responses:
        "200":
          description: One decision per device
          content:
            application/json:
              schema:
                type: object
                properties:
                  app:
                    type: string
                  channel:
                    type: string
                  decisions:
                    type: array
                    items:
                      $ref: "#/components/schemas/Decision"
                  updatesOffered:
                    type: integer
        "400":
          description: Validation failed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Invalid or revoked API key
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Unknown app or channel
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /u/{appRef}/{channel}/{platform}/{arch}/{version}:
    get:
      operationId: checkForUpdate
      tags: [Device endpoints]
      summary: Device update check
      description: |
        The entire client contract. `200` returns an update manifest,
        `204` means up to date or not in the current rollout cohort.
        Every check doubles as the heartbeat behind the fleet dashboard.

        This endpoint **is** Tauri's dynamic-updater contract.
      parameters:
        - name: appRef
          in: path
          required: true
          schema:
            type: string
        - name: channel
          in: path
          required: true
          schema:
            type: string
        - name: platform
          in: path
          required: true
          schema:
            type: string
        - name: arch
          in: path
          required: true
          schema:
            type: string
        - name: version
          in: path
          required: true
          schema:
            type: string
          description: Version the device currently runs
        - name: X-Relayer-Device
          in: header
          schema:
            type: string
          description: |
            Anonymous UUID generated and persisted by your app. Required to
            join staged-rollout cohorts and the fleet dashboard.
      responses:
        "200":
          description: Update available
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/UpdateManifest"
        "204":
          description: Up to date (or not in the rollout cohort yet)
        "404":
          description: Unknown app or channel
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /e/{appRef}/{channel}/{file}:
    get:
      operationId: electronFeed
      tags: [Device endpoints]
      summary: Electron update feed
      description: |
        electron-builder generic-provider feed. `file` is `latest.yml`,
        `latest-mac.yml` or `latest-linux.yml`. Point
        `publish.provider: generic` at `/e/{appRef}/{channel}`.
      parameters:
        - name: appRef
          in: path
          required: true
          schema:
            type: string
        - name: channel
          in: path
          required: true
          schema:
            type: string
        - name: file
          in: path
          required: true
          schema:
            type: string
            enum: [latest.yml, latest-mac.yml, latest-linux.yml]
      responses:
        "200":
          description: YAML feed for the offered version
          content:
            text/yaml:
              schema:
                type: string
        "404":
          description: Unknown app, channel or file
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /feed/{appRef}/{channel}/rss.xml:
    get:
      operationId: releaseNotesRss
      tags: [Device endpoints]
      summary: Release-notes feed (RSS)
      description: |
        The release-notes feed as RSS 2.0. Subscribe to an app's changelog
        from any feed reader or a Slack RSS integration. Same data as the
        JSON feed. No auth, CORS-open, cached 60s.
      parameters:
        - name: appRef
          in: path
          required: true
          schema:
            type: string
        - name: channel
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: RSS 2.0 feed of published releases, newest first
          content:
            application/rss+xml:
              schema:
                type: string
        "404":
          description: Unknown app or channel
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /feed/{appRef}/{channel}:
    get:
      operationId: releaseNotesFeed
      tags: [Device endpoints]
      summary: Release-notes feed
      description: |
        Published releases, newest first, with version, markdown notes,
        date and artifacts. No auth, CORS-open, cached 60s. Built for
        "What's new" screens and public changelogs.
      parameters:
        - name: appRef
          in: path
          required: true
          schema:
            type: string
        - name: channel
          in: path
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Published releases
          content:
            application/json:
              schema:
                type: object
                properties:
                  app:
                    type: string
                  channel:
                    type: string
                  releases:
                    type: array
                    items:
                      $ref: "#/components/schemas/Release"
        "404":
          description: Unknown app or channel
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

webhooks:
  github-release:
    post:
      operationId: githubReleaseInbound
      tags: [Releases]
      summary: GitHub release webhook (inbound)
      description: |
        Relayer receives GitHub `release` events at
        `/wh/github/{appId}` (URL + secret shown when you enable the
        integration in app settings). Payloads are HMAC-SHA-256 verified.
        Published releases land on `stable`, pre-releases on `beta`,
        drafts are ignored, assets are classified per platform/arch by
        filename, and checksum/metadata files are filtered out.
      requestBody:
        content:
          application/json:
            schema:
              type: object
              description: Standard GitHub release event payload
      responses:
        "200":
          description: "Ingested (created / duplicate / ignored)"
