asyncapi: 3.0.0
info:
  title: Moda Async API
  version: 1.0.0
  description: >
    Real-time async API over Phoenix Channels. Covers collaborative document
    editing via Yjs CRDT, server-push BEAM/host telemetry metrics via OTLP JSON,
    etc.
  tags:
    - name: yjs
      description: Collaborative editing transport over Phoenix Channels
    - name: telemetry
      description: Real-time BEAM and host metrics server-push
    - name: chart
      description: Live crypto price streaming via Binance proxy
  license:
    name: MIT
servers:
  production:
    host: "{domain}:{port}"
    pathname: "/socket/websocket"
    protocol: wss
    description: Phoenix WebSocket endpoint
    variables:
      domain:
        $ref: "#/components/serverVariables/domain"
      port:
        default: "443"
    bindings:
      ws:
        method: GET
        query:
          $ref: "#/components/schemas/socketConnectQuery"
        bindingVersion: 0.1.0
  development:
    host: "{domain}:{port}"
    pathname: "/socket/websocket"
    protocol: ws
    description: Local development server
    variables:
      domain:
        default: localhost
      port:
        default: "4001"
        description: >
          Phoenix dev server port. Configured via PORT env var in
          phoenix_framework/config/dev.exs.
    bindings:
      ws:
        method: GET
        query:
          $ref: "#/components/schemas/socketConnectQuery"
        bindingVersion: 0.1.0
defaultContentType: application/octet-stream
channels:
  yDocRoom:
    address: "y_doc_room:{docName}"
    title: Yjs Document Room
    description: >
      Phoenix Channel for collaborative document editing.

      Each document has its own room identified by `docName`.

      Example topics: `y_doc_room:excalidraw:my-board`,
      `y_doc_room:notes:draft`.
    parameters:
      docName:
        description: >
          Unique document identifier. Can be hierarchical (e.g.
          `excalidraw:board-1`). Expected format: `[A-Za-z0-9:_-]+`, length
          1..255. Example values: `excalidraw:default`, `excalidraw:board-1`,
          `notes:draft`.
    messages:
      yjsSyncIn:
        $ref: "#/components/messages/yjsSyncIn"
      yjsUpdateIn:
        $ref: "#/components/messages/yjsUpdateIn"
      yjsUpdateOut:
        $ref: "#/components/messages/yjsUpdateOut"
      phxJoin:
        $ref: "#/components/messages/phxJoin"
      phxJoinReply:
        $ref: "#/components/messages/phxJoinReply"
      phxError:
        $ref: "#/components/messages/phxError"
      phxClose:
        $ref: "#/components/messages/phxClose"
    bindings:
      ws:
        method: GET
        bindingVersion: 0.1.0
  telemetryMetrics:
    address: "telemetry:metrics"
    title: Telemetry Metrics Stream
    description: >
      Server-push Phoenix Channel for real-time BEAM runtime and host system
      metrics. Server pushes one chart-ready metric point per event at a fixed
      interval (default 2s). No client-to-server data messages - pure
      server-push pattern.

      Metrics include host CPU/memory, BEAM process count/memory/run queue, HTTP
      request latency percentiles, and WebSocket connection count.
    messages:
      metric:
        $ref: "#/components/messages/metric"
      telemetryJoin:
        $ref: "#/components/messages/telemetryJoin"
      telemetryJoinReply:
        $ref: "#/components/messages/telemetryJoinReply"
      phxError:
        $ref: "#/components/messages/phxError"
      phxClose:
        $ref: "#/components/messages/phxClose"
    bindings:
      ws:
        method: GET
        bindingVersion: 0.1.0
  chartPrices:
    address: "chart:prices"
    title: Crypto Price Stream
    description: >
      Server-push Phoenix Channel for live crypto price data. Backend proxies
      Binance WebSocket
      (wss://stream.binance.com:9443/stream?streams=btcusdt@miniTicker/ethusdt@miniTicker)
      and pushes raw miniTicker events as JSON.
    messages:
      priceTickOut:
        $ref: "#/components/messages/priceTickOut"
      chartJoin:
        $ref: "#/components/messages/chartJoin"
      chartJoinReply:
        $ref: "#/components/messages/chartJoinReply"
      phxError:
        $ref: "#/components/messages/phxError"
      phxClose:
        $ref: "#/components/messages/phxClose"
    bindings:
      ws:
        method: GET
        bindingVersion: 0.1.0
operations:
  joinRoom:
    action: send
    channel:
      $ref: "#/channels/yDocRoom"
    summary: Join a document room
    description: |
      Client joins a Yjs document room. Server initializes or looks up
      the SharedDoc process via DynamicSupervisor and subscribes to PubSub.
    messages:
      - $ref: "#/channels/yDocRoom/messages/phxJoin"
    reply:
      channel:
        $ref: "#/channels/yDocRoom"
      messages:
        - $ref: "#/channels/yDocRoom/messages/phxJoinReply"
  sendYjsSync:
    action: send
    channel:
      $ref: "#/channels/yDocRoom"
    summary: Initiate Yjs sync handshake
    description: |
      Client sends a Yjs sync step (step1 or step2) as binary data.
      The server forwards it to the SharedDoc GenServer which responds
      with the appropriate sync reply via `handle_info`.
    messages:
      - $ref: "#/channels/yDocRoom/messages/yjsSyncIn"
  sendYjsUpdate:
    action: send
    channel:
      $ref: "#/channels/yDocRoom"
    summary: Send a Yjs document update
    description: |
      Client sends a Yjs update (awareness or doc mutation) as binary data.
      The server applies it to the SharedDoc and broadcasts to all other
      clients in the room via Phoenix.PubSub.
    messages:
      - $ref: "#/channels/yDocRoom/messages/yjsUpdateIn"
  receiveYjsUpdate:
    action: receive
    channel:
      $ref: "#/channels/yDocRoom"
    summary: Receive a Yjs document update
    description: |
      Server pushes Yjs updates to the client. Sources:
      - PubSub broadcast from another client's update (`{:yjs_update, chunk}`)
      - Direct message from SharedDoc process (`{:yjs, chunk, pid}`)
    messages:
      - $ref: "#/channels/yDocRoom/messages/yjsUpdateOut"
  receiveChannelError:
    action: receive
    channel:
      $ref: "#/channels/yDocRoom"
    summary: Receive channel-level error event
    description: |
      Phoenix protocol-level channel failure event.
      Can happen on transport disruption or internal channel crashes.
    messages:
      - $ref: "#/channels/yDocRoom/messages/phxError"
  receiveChannelClose:
    action: receive
    channel:
      $ref: "#/channels/yDocRoom"
    summary: Receive channel close event
    description: |
      Phoenix protocol-level close event signaling channel shutdown
      or explicit leave.
    messages:
      - $ref: "#/channels/yDocRoom/messages/phxClose"
  joinTelemetry:
    action: send
    channel:
      $ref: "#/channels/telemetryMetrics"
    summary: Join the telemetry metrics channel
    description: |
      Client joins the telemetry metrics channel. No authentication required.
      Server begins pushing metric chunks immediately after join reply.
      No catch-up or historical data - stream starts from the moment of join.
    messages:
      - $ref: "#/channels/telemetryMetrics/messages/telemetryJoin"
    reply:
      channel:
        $ref: "#/channels/telemetryMetrics"
      messages:
        - $ref: "#/channels/telemetryMetrics/messages/telemetryJoinReply"
  receiveMetrics:
    action: receive
    channel:
      $ref: "#/channels/telemetryMetrics"
    summary: Receive telemetry metric chunk
    description: |
      Server pushes one chart-ready telemetry metric point per event at a fixed
      interval (default 2s). Each payload includes metric identity, unit,
      normalized Unix timestamp, and series values for rendering.
    tags:
      - $ref: "#/info/tags/1"
    messages:
      - $ref: "#/channels/telemetryMetrics/messages/metric"
  joinChart:
    action: send
    channel:
      $ref: "#/channels/chartPrices"
    summary: Join the crypto price stream channel
    description: |
      Client joins the chart prices channel. No authentication required.
      Server confirms join and then pushes price_tick events on each
      Binance miniTicker update (~1s).
    messages:
      - $ref: "#/channels/chartPrices/messages/chartJoin"
    reply:
      channel:
        $ref: "#/channels/chartPrices"
      messages:
        - $ref: "#/channels/chartPrices/messages/chartJoinReply"
  receivePriceTick:
    action: receive
    channel:
      $ref: "#/channels/chartPrices"
    summary: Receive a crypto price tick
    description: |
      Server pushes a price tick on each Binance miniTicker update (~1s per symbol).
      Each tick is a raw Binance 24hr miniTicker payload.
    tags:
      - $ref: "#/info/tags/2"
    messages:
      - $ref: "#/channels/chartPrices/messages/priceTickOut"
components:
  serverVariables:
    domain:
      description: Server hostname. Set via PHX_HOST env var.
    port:
      default: "4001"
      description: Server port. Set via PORT env var.
  messages:
    chartJoin:
      name: phx_join
      title: Join Chart Prices Channel
      summary: Request to join the crypto price stream channel
      contentType: application/json
      payload:
        $ref: "#/components/schemas/joinPayload"
      examples:
        - name: chart-join
          summary: Join chart prices channel
          payload: {}
    chartJoinReply:
      name: phx_reply
      title: Chart Join Reply
      summary: Server confirms chart channel join
      contentType: application/json
      payload:
        $ref: "#/components/schemas/chartJoinReplyPayload"
      examples:
        - name: chart-join-ok
          summary: Successful join
          payload:
            status: ok
            response: {}
    priceTickOut:
      name: price_tick
      title: Price Tick
      summary: Server-push crypto price update
      description: |
        Real-time price update from Binance WebSocket proxy.
        Pushed on each miniTicker update (~1s per symbol).
        Raw Binance 24hr miniTicker event forwarded as-is.
      contentType: application/json
      payload:
        $ref: "#/components/schemas/priceTick"
      examples:
        - name: price-tick-btc
          summary: BTC miniTicker event
          payload:
            e: 24hrMiniTicker
            E: 1740441600123
            s: BTCUSDT
            c: "96543.21"
            o: "96000.00"
            h: "97000.00"
            l: "95500.00"
            v: "12345.678"
            q: "1190123456.78"
        - name: price-tick-eth
          summary: ETH miniTicker event
          payload:
            e: 24hrMiniTicker
            E: 1740441600456
            s: ETHUSDT
            c: "3421.56"
            o: "3400.00"
            h: "3480.00"
            l: "3390.00"
            v: "98765.432"
            q: "340123456.78"
    telemetryJoin:
      name: phx_join
      title: Join Telemetry Channel
      summary: Request to join the telemetry metrics channel
      contentType: application/json
      payload:
        $ref: "#/components/schemas/joinPayload"
      examples:
        - name: telemetry-join
          summary: Join telemetry channel
          payload: {}
    telemetryJoinReply:
      name: phx_reply
      title: Telemetry Join Reply
      summary: Server confirms telemetry channel join
      contentType: application/json
      payload:
        $ref: "#/components/schemas/joinReply"
      examples:
        - name: telemetry-join-ok
          summary: Successful join
          payload:
            status: ok
            response: {}
    metric:
      name: metric
      title: Telemetry Metric UI Point
      summary: Server-push single chart-ready telemetry metric point
      description: |
        Periodic server-push event containing a single BEAM runtime or host
        system metric formatted for frontend chart rendering. Pushed every
        500ms (configurable).

        Metrics included:
        - system.cpu.load_average.1m
        - system.memory.usage
        - process.runtime.beam.memory.total
        - process.runtime.beam.process_count
      contentType: application/json
      payload:
        $ref: "#/components/schemas/telemetryMetricPoint"
      examples:
        - name: metric-ui-cpu
          summary: Single metric point for CPU load average
          payload:
            name: system.cpu.load_average.1m
            unit: ""
            tsUnixSec: 1740441600.0
            series:
              - key: value
                value: 0.42
        - name: metric-ui-http-latency
          summary: Latency percentiles projected as two chart series
          payload:
            name: http.server.duration
            unit: ms
            tsUnixSec: 1740441600.12
            series:
              - key: p50
                value: 12.3
              - key: p95
                value: 45.8
    phxJoin:
      name: phx_join
      title: Join Room
      summary: Request to join a Yjs document room
      description: |
        Phoenix Channel join event. The `docName` from the topic
        is used to look up or start a SharedDoc GenServer.
      contentType: application/json
      payload:
        $ref: "#/components/schemas/joinPayload"
      bindings:
        ws:
          bindingVersion: 0.1.0
      examples:
        - name: join-with-empty-payload
          summary: Typical join without additional params
          payload: {}
        - name: join-with-meta
          summary: Join with optional client metadata
          payload:
            client:
              name: web-nextjs
    phxJoinReply:
      name: phx_reply
      title: Join Reply
      summary: Server confirms or rejects the join
      contentType: application/json
      payload:
        $ref: "#/components/schemas/joinReply"
      examples:
        - name: join-ok
          summary: Successful join response
          payload:
            status: ok
            response: {}
        - name: join-error
          summary: Join rejected by channel
          payload:
            status: error
            response:
              reason: failed to initialize document
    phxError:
      name: phx_error
      title: Channel Error
      summary: Channel-level error event from Phoenix protocol
      contentType: application/json
      payload:
        $ref: "#/components/schemas/phxSystemPayload"
      examples:
        - name: channel-error
          payload:
            reason: internal_error
    phxClose:
      name: phx_close
      title: Channel Closed
      summary: Channel close event from Phoenix protocol
      contentType: application/json
      payload:
        $ref: "#/components/schemas/phxSystemPayload"
      examples:
        - name: channel-close
          payload:
            reason: closed
    yjsSyncIn:
      name: yjs_sync
      title: Yjs Sync Message (Client to Server)
      summary: Binary Yjs sync protocol message
      description: |
        Yjs sync frame encoded with lib0 varUint format.
        Top-level type is `0` (sync), followed by nested sync subtype:
        - `0`: Sync Step 1 (state vector)
        - `1`: Sync Step 2 (delta for given vector)
        - `2`: Document update
      contentType: application/octet-stream
      payload:
        $ref: "#/components/schemas/yjsBinaryPayload"
      examples:
        - name: sync-step1-base64
          summary: Example binary frame encoded as base64 for documentation
          payload: AAE=
    yjsUpdateIn:
      name: yjs
      title: Yjs Update (Client to Server)
      summary: Binary Yjs document update or awareness message
      description: |
        Binary Yjs frame encoded with lib0 varUint format.
        Top-level type can be:
        - `0`: sync message (including update subtype `2`)
        - `1`: awareness update
        - `3`: awareness query
        The payload is applied to SharedDoc for CRDT merge and persistence.
        Broadcast to all other clients via Phoenix.PubSub.
        Persisted to `yjs_records` table via Yex persistence layer.
      contentType: application/octet-stream
      payload:
        $ref: "#/components/schemas/yjsBinaryPayload"
      examples:
        - name: awareness-update-base64
          summary: Example binary frame encoded as base64 for documentation
          payload: AQID
    yjsUpdateOut:
      name: yjs
      title: Yjs Update (Server to Client)
      summary: Binary Yjs document update pushed to client
      description: |
        Binary Yjs frame in the same format as incoming `yjs` event.
        Originates from another collaborator, SharedDoc sync response,
        or awareness propagation.
      contentType: application/octet-stream
      payload:
        $ref: "#/components/schemas/yjsBinaryPayload"
      examples:
        - name: sync-reply-base64
          summary: Example binary frame encoded as base64 for documentation
          payload: AAECAw==
  schemas:
    socketConnectQuery:
      type: object
      properties:
        vsn:
          type: string
          description: Phoenix serializer version
          const: "2.0.0"
        token:
          type: string
          description: Optional auth token for socket connect
      required:
        - vsn
      additionalProperties: true
    yjsBinaryPayload:
      type: string
      format: binary
      description: |
        Yjs frame serialized in binary.
        Encoding uses varUint message prefixes (not JSON arrays):
        - Top-level `0` => sync protocol message
        - Top-level `1` => awareness protocol update
        - Top-level `3` => awareness query
        For sync messages, nested sync subtypes are:
        - `0` => sync step 1
        - `1` => sync step 2
        - `2` => update
        See https://github.com/yjs/y-protocols for encoding details.
    joinPayload:
      type: object
      description: Optional join payload accepted by Phoenix channel join
      additionalProperties: true
    joinReply:
      oneOf:
        - type: object
          properties:
            status:
              const: ok
            response:
              type: object
              additionalProperties: true
          required:
            - status
            - response
        - type: object
          properties:
            status:
              const: error
            response:
              type: object
              properties:
                reason:
                  type: string
                  description:
                    Error reason (e.g. "failed to initialize document")
              required:
                - reason
          required:
            - status
            - response
    phxSystemPayload:
      type: object
      description: Phoenix protocol payload for channel lifecycle events
      additionalProperties: true
    priceTick:
      type: object
      description: |
        Binance 24hr miniTicker event forwarded as-is.
        See https://developers.binance.com/docs/binance-spot-api-docs/web-socket-streams#individual-symbol-mini-ticker-stream
      required:
        - e
        - E
        - s
        - c
        - o
        - h
        - l
        - v
        - q
      properties:
        e:
          type: string
          description: Event type (always "24hrMiniTicker")
        E:
          type: integer
          description: Event time in milliseconds since epoch
        s:
          type: string
          description: Symbol (e.g. "BTCUSDT", "ETHUSDT")
        c:
          type: string
          description: Close price (current price)
        o:
          type: string
          description: Open price (24h)
        h:
          type: string
          description: High price (24h)
        l:
          type: string
          description: Low price (24h)
        v:
          type: string
          description: Total traded base asset volume
        q:
          type: string
          description: Total traded quote asset volume
    chartJoinReplyPayload:
      type: object
      properties:
        status:
          const: ok
        response:
          type: object
          additionalProperties: true
      required:
        - status
        - response
    otelExportMetricsServiceRequest:
      type: object
      description: |
        OTLP JSON metrics payload following the OpenTelemetry Metrics Data Model.
        See https://opentelemetry.io/docs/specs/otlp/#json-protobuf-encoding
      required:
        - resourceMetrics
      properties:
        resourceMetrics:
          type: array
          items:
            $ref: "#/components/schemas/otelResourceMetrics"
    otelResourceMetrics:
      type: object
      required:
        - resource
        - scopeMetrics
      properties:
        resource:
          type: object
          properties:
            attributes:
              type: array
              items:
                $ref: "#/components/schemas/otelKeyValue"
        scopeMetrics:
          type: array
          items:
            type: object
            properties:
              scope:
                type: object
                properties:
                  name:
                    type: string
                  version:
                    type: string
              metrics:
                type: array
                items:
                  $ref: "#/components/schemas/otelMetric"
    telemetryMetricPoint:
      type: object
      description: |
        Chart-ready telemetry payload used by frontend clients. This shape is
        a UI projection and does not expose OTLP datapoint internals.
      required:
        - name
        - unit
        - tsUnixSec
        - series
      properties:
        name:
          type: string
          description: Metric name used for chart routing and widget identity.
        unit:
          type: string
          description:
            Unit string used for display formatting. Empty for dimensionless
            metrics.
        tsUnixSec:
          type: number
          description: Unix timestamp in seconds as a JS-safe numeric value.
        series:
          type: array
          minItems: 1
          items:
            $ref: "#/components/schemas/telemetrySeriesPoint"
    telemetrySeriesPoint:
      type: object
      required:
        - key
        - value
      properties:
        key:
          type: string
          description: Series key (for example value, p50, p95).
        value:
          type: number
          description: Numeric value for the series at tsUnixSec.
    otelMetric:
      type: object
      description: |
        A single metric in OTLP format. Contains name, unit, and one of
        the metric type fields (gauge, sum, histogram, etc.).
        This implementation uses gauge for all metrics including pre-aggregated
        histogram percentiles.
      required:
        - name
        - gauge
      properties:
        name:
          type: string
          description: |
            Metric name following OTel semantic conventions.
            Examples: system.cpu.load_average.1m, process.runtime.beam.process_count
        unit:
          type: string
          description: |
            OTel unit string. Examples: "1" (dimensionless), "By" (bytes),
            "ms" (milliseconds), "{process}" (count of processes)
        gauge:
          type: object
          properties:
            dataPoints:
              type: array
              items:
                $ref: "#/components/schemas/otelNumberDataPoint"
    otelNumberDataPoint:
      type: object
      description: A single data point within a metric
      required:
        - timeUnixNano
      properties:
        timeUnixNano:
          type: string
          description:
            Timestamp as nanoseconds since Unix epoch, encoded as string
        asDouble:
          type: number
          description: Floating-point metric value
        asInt:
          type: string
          description:
            Integer metric value, encoded as string per OTLP convention
        attributes:
          type: array
          items:
            $ref: "#/components/schemas/otelKeyValue"
    otelKeyValue:
      type: object
      description: OTel key-value attribute pair
      required:
        - key
        - value
      properties:
        key:
          type: string
        value:
          type: object
          description:
            Typed value (stringValue, intValue, doubleValue, boolValue)
          properties:
            stringValue:
              type: string
            intValue:
              type: string
            doubleValue:
              type: number
            boolValue:
              type: boolean
  securitySchemes:
    socketToken:
      type: httpApiKey
      name: token
      in: query
      description: |
        Optional token passed as query parameter during WebSocket connect.
        Currently unused (socket.connect returns {:ok, socket} unconditionally).
