asyncapi: 3.0.0
info:
  title: Aoba LiveStore Sync WebSocket API
  version: 1.0.0
  description: |
    LiveStore event synchronization WebSocket API for cross-device data sync.

    ## Overview

    The LiveStore sync WebSocket enables real-time event synchronization between
    a user's devices using an event-sourced architecture. This channel operates as
    a **peer-to-peer relay** where the server routes messages between devices without
    persisting events.

    ## Architecture

    ```
    Device A ──┐                    ┌── Device B
               │   ┌──────────┐     │
               ├──>│  Server  │<────┤
               │   │ (Relay)  │     │
               │   └──────────┘     │
    Device C ──┘                    └── Device D
    ```

    - **Event-Sourced**: All state changes are events in an append-only log
    - **Local-First**: Each device maintains its own SQLite database
    - **Peer-to-Peer Sync**: Devices sync history directly through server relay
    - **No Server Persistence**: Server only routes messages, doesn't store events

    ## Connection

    - **Endpoint**: `/api/v1/livestore/sync`
    - **Protocol**: WebSocket (RFC 6455)
    - **Authentication**: JWT token via `auth_token` cookie or `?token=` query parameter

    ### Connection Examples

    **Same-origin (cookie-based):**
    ```javascript
    const ws = new WebSocket('wss://api.example.com/api/v1/livestore/sync');
    ```

    **Cross-origin (token parameter):**
    ```javascript
    const ws = new WebSocket(`wss://api.example.com/api/v1/livestore/sync?token=${jwtToken}`);
    ```

    ## Message Flow

    ### Real-time Sync (Push)

    ```
    Device A              Server              Device B
       │                    │                    │
       │──── push ─────────>│                    │
       │                    │──── push ─────────>│
       │<─── ack ───────────│                    │
       │                    │                    │
    ```

    ### History Sync (Peer Pull)

    ```
    New Device            Server            Existing Device
       │                    │                    │
       │── peer_pull ──────>│                    │
       │                    │── peer_pull ──────>│
       │                    │<─ peer_pull_resp ──│
       │<─ peer_pull_resp ──│                    │
       │                    │                    │
    ```

    ## Protocol Reference

    This implementation follows the @livestore/adapter-web protocol specification
    with custom extensions for peer-to-peer history sync.

    ### Client Identification

    Each client must identify itself on first message:
    - `clientId`: Unique device identifier (persisted per device)
    - `sessionId`: Current session identifier (changes on reconnect)

  contact:
    name: Aoba API Team
  license:
    name: Proprietary

servers:
  production:
    host: api.example.com
    protocol: wss
    description: Production LiveStore sync server
    security:
      - $ref: '#/components/securitySchemes/jwtCookie'
  development:
    host: localhost:8080
    protocol: ws
    description: Development LiveStore sync server
    security:
      - $ref: '#/components/securitySchemes/jwtCookie'

channels:
  livestoreSync:
    address: /api/v1/livestore/sync
    title: LiveStore Sync Channel
    description: |
      Bidirectional WebSocket channel for LiveStore event synchronization.

      This channel supports both sending and receiving messages for full
      peer-to-peer event synchronization.
    messages:
      push:
        $ref: '#/components/messages/Push'
      pull:
        $ref: '#/components/messages/Pull'
      pullResponse:
        $ref: '#/components/messages/PullResponse'
      ack:
        $ref: '#/components/messages/Ack'
      peerPull:
        $ref: '#/components/messages/PeerPull'
      peerPullResponse:
        $ref: '#/components/messages/PeerPullResponse'
      error:
        $ref: '#/components/messages/Error'
    servers:
      - $ref: '#/servers/production'
      - $ref: '#/servers/development'

operations:
  sendSyncMessage:
    action: send
    channel:
      $ref: '#/channels/livestoreSync'
    title: Send Sync Message
    summary: Send sync messages to the server for relay
    description: |
      Send events or sync requests to the server.

      ## Message Types

      | Type | Direction | Description |
      |------|-----------|-------------|
      | `push` | Client -> Server -> Peers | Push new events to other devices |
      | `pull` | Client -> Server | Request events from server (returns empty + peer list) |
      | `peer_pull` | Client -> Server -> Peers | Request history from online peers |
      | `peer_pull_response` | Client -> Server -> Requester | Respond to peer pull with history |

      ## Push Example

      ```javascript
      ws.send(JSON.stringify({
        type: 'push',
        clientId: 'device-abc-123',
        sessionId: 'session-xyz',
        timestamp: Date.now(),
        payload: {
          events: [{
            eventName: 'message.created',
            eventNumber: 42,
            clientId: 'device-abc-123',
            sessionId: 'session-xyz',
            data: { conversationId: '...', text: 'Hello!' },
            timestamp: Date.now()
          }]
        }
      }));
      ```
    messages:
      - $ref: '#/channels/livestoreSync/messages/push'
      - $ref: '#/channels/livestoreSync/messages/pull'
      - $ref: '#/channels/livestoreSync/messages/peerPull'
      - $ref: '#/channels/livestoreSync/messages/peerPullResponse'

  receiveSyncMessage:
    action: receive
    channel:
      $ref: '#/channels/livestoreSync'
    title: Receive Sync Message
    summary: Receive sync messages from server
    description: |
      Receive events and responses relayed from other devices.

      ## Message Types

      | Type | Source | Description |
      |------|--------|-------------|
      | `push` | Peer device | Events pushed by another device |
      | `pull_response` | Server | Response to pull request (includes online peer list) |
      | `ack` | Server | Acknowledgment of received push |
      | `peer_pull` | Peer device | History request from another device |
      | `peer_pull_response` | Peer device | History response from peer |
      | `error` | Server | Error response |

      ## Message Handling Example

      ```javascript
      ws.onmessage = (event) => {
        const msg = JSON.parse(event.data);

        switch (msg.type) {
          case 'push':
            // Apply events from peer device
            applyEvents(msg.payload.events);
            break;

          case 'ack':
            // Push was acknowledged
            console.log(`${msg.payload.receivedCount} events received`);
            break;

          case 'pull_response':
            // Server has no events but provides peer list
            if (msg.onlinePeers?.length > 0) {
              requestHistoryFromPeer(msg.onlinePeers[0]);
            }
            break;

          case 'peer_pull':
            // Another device wants history
            respondWithHistory(msg.payload.requestId, msg.payload.sinceTimestamp);
            break;

          case 'peer_pull_response':
            // Received history from peer
            applyEvents(msg.payload.events);
            break;

          case 'error':
            console.error(`Sync error: ${msg.code} - ${msg.message}`);
            break;
        }
      };
      ```
    messages:
      - $ref: '#/channels/livestoreSync/messages/push'
      - $ref: '#/channels/livestoreSync/messages/pullResponse'
      - $ref: '#/channels/livestoreSync/messages/ack'
      - $ref: '#/channels/livestoreSync/messages/peerPull'
      - $ref: '#/channels/livestoreSync/messages/peerPullResponse'
      - $ref: '#/channels/livestoreSync/messages/error'

components:
  securitySchemes:
    jwtCookie:
      type: httpApiKey
      name: auth_token
      in: cookie
      description: |
        JWT token stored in HTTP-only cookie named `auth_token`.

        For cross-origin WebSocket connections, the token can also be
        passed as a `?token=` query parameter.

  messages:
    Push:
      name: push
      title: Push Events
      summary: Push new events to other devices via server relay
      description: |
        Send new events from this device to be relayed to all other online
        devices of the same user. The server does not persist events.
      contentType: application/json
      payload:
        $ref: '#/components/schemas/PushMessage'
      examples:
        - name: PushMessageExample
          summary: Push a new message event
          payload:
            type: push
            clientId: "device-abc-123"
            sessionId: "session-xyz-456"
            timestamp: 1733644800000
            payload:
              events:
                - eventName: "message.created"
                  eventNumber: 42
                  clientId: "device-abc-123"
                  sessionId: "session-xyz-456"
                  data:
                    conversationId: "conv-123"
                    text: "Hello world!"
                  timestamp: 1733644800000

    Pull:
      name: pull
      title: Pull Request
      summary: Request events from server
      description: |
        Request events since a timestamp. Since the server doesn't persist
        events, this returns an empty list along with a list of online peers
        that can provide history.
      contentType: application/json
      payload:
        $ref: '#/components/schemas/PullMessage'
      examples:
        - name: PullRequestExample
          summary: Request events since a timestamp
          payload:
            type: pull
            clientId: "device-abc-123"
            sessionId: "session-xyz-456"
            timestamp: 1733644800000
            payload:
              sinceTimestamp: 1733640000000

    PullResponse:
      name: pull_response
      title: Pull Response
      summary: Response to pull request with events and peer list
      description: |
        Server response to a pull request. Since the server doesn't persist
        events, this typically contains an empty event list but includes
        a list of online peers that can provide history via `peer_pull`.
      contentType: application/json
      payload:
        $ref: '#/components/schemas/PullResponseMessage'
      examples:
        - name: PullResponseExample
          summary: Pull response with online peers
          payload:
            type: pull_response
            clientId: "server"
            sessionId: ""
            timestamp: 1733644800000
            payload:
              events: []
              fromClientId: "server"
              hasMore: false
            onlinePeers:
              - "device-def-789"
              - "device-ghi-012"

    Ack:
      name: ack
      title: Acknowledgment
      summary: Server acknowledgment of received push
      description: |
        Sent by the server to acknowledge receipt of a push message.
        Confirms how many events were received and relayed.
      contentType: application/json
      payload:
        $ref: '#/components/schemas/AckMessage'
      examples:
        - name: AckExample
          summary: Acknowledgment of 3 events
          payload:
            type: ack
            clientId: "server"
            sessionId: ""
            timestamp: 0
            payload:
              receivedCount: 3
              lastTimestamp: 0

    PeerPull:
      name: peer_pull
      title: Peer Pull Request
      summary: Request history from online peer devices
      description: |
        Request event history directly from other online devices of the same
        user. The server routes this request to all online peers.

        This enables new devices to sync historical data from existing devices
        without requiring server-side event storage.
      contentType: application/json
      payload:
        $ref: '#/components/schemas/PeerPullMessage'
      examples:
        - name: PeerPullExample
          summary: Request history from peers
          payload:
            type: peer_pull
            clientId: "device-new-123"
            sessionId: "session-xyz-456"
            timestamp: 1733644800000
            payload:
              requestId: "req-abc-123"
              sinceTimestamp: 1733640000000
              retentionDays: 30

    PeerPullResponse:
      name: peer_pull_response
      title: Peer Pull Response
      summary: History response from a peer device
      description: |
        Response to a peer pull request containing historical events from
        another device. The server routes this back to the requesting device.
      contentType: application/json
      payload:
        $ref: '#/components/schemas/PeerPullResponseMessage'
      examples:
        - name: PeerPullResponseExample
          summary: History response with events
          payload:
            type: peer_pull_response
            clientId: "device-def-789"
            sessionId: "session-abc-123"
            timestamp: 1733644800000
            payload:
              requestId: "req-abc-123"
              events:
                - eventName: "message.created"
                  eventNumber: 40
                  clientId: "device-def-789"
                  sessionId: "session-old-456"
                  data:
                    conversationId: "conv-123"
                    text: "Earlier message"
                  timestamp: 1733641000000
              hasMore: false

    Error:
      name: error
      title: Error Message
      summary: Error response from server
      description: |
        Sent when the server encounters an error processing a message.
      contentType: application/json
      payload:
        $ref: '#/components/schemas/ErrorMessage'
      examples:
        - name: ErrorExample
          summary: Invalid message error
          payload:
            type: error
            code: "invalid_message"
            message: "Failed to parse message"

  schemas:
    LiveStoreMessageType:
      type: string
      description: The type of LiveStore sync message
      enum:
        - push
        - pull
        - pull_response
        - ack
        - peer_pull
        - peer_pull_response
      x-enum-descriptions:
        push: Push events from client to server for relay to peers
        pull: Request events from server (returns empty + peer list)
        pull_response: Response to pull request
        ack: Acknowledgment of received events
        peer_pull: Request history from peer devices
        peer_pull_response: Response to peer pull with history

    LiveStoreMessageBase:
      type: object
      description: Base structure for all LiveStore messages
      required:
        - type
        - clientId
        - sessionId
        - timestamp
      properties:
        type:
          $ref: '#/components/schemas/LiveStoreMessageType'
        clientId:
          type: string
          description: |
            Unique device identifier. Should be persisted per device installation.
          example: "device-abc-123"
        sessionId:
          type: string
          description: |
            Current session identifier. Changes on each WebSocket reconnection.
          example: "session-xyz-456"
        timestamp:
          type: integer
          format: int64
          description: Unix timestamp in milliseconds
          example: 1733644800000
        payload:
          type: object
          description: Message-specific payload

    LiveStoreEvent:
      type: object
      description: A single event in the LiveStore eventlog
      required:
        - eventName
        - eventNumber
        - clientId
        - sessionId
        - data
        - timestamp
      properties:
        eventName:
          type: string
          description: Name/type of the event (e.g., "message.created")
          example: "message.created"
        eventNumber:
          type: integer
          format: int64
          description: Monotonically increasing event number per client
          example: 42
        clientId:
          type: string
          description: Device that originated this event
          example: "device-abc-123"
        sessionId:
          type: string
          description: Session that originated this event
          example: "session-xyz-456"
        data:
          type: object
          description: Event-specific data payload
          additionalProperties: true
        timestamp:
          type: integer
          format: int64
          description: Unix timestamp in milliseconds when event was created
          example: 1733644800000

    PushMessage:
      allOf:
        - $ref: '#/components/schemas/LiveStoreMessageBase'
        - type: object
          properties:
            type:
              type: string
              const: push
            payload:
              $ref: '#/components/schemas/PushPayload'

    PushPayload:
      type: object
      description: Payload for push messages
      required:
        - events
      properties:
        events:
          type: array
          description: List of events to push to other devices
          items:
            $ref: '#/components/schemas/LiveStoreEvent'

    PullMessage:
      allOf:
        - $ref: '#/components/schemas/LiveStoreMessageBase'
        - type: object
          properties:
            type:
              type: string
              const: pull
            payload:
              $ref: '#/components/schemas/PullPayload'

    PullPayload:
      type: object
      description: Payload for pull requests
      required:
        - sinceTimestamp
      properties:
        sinceTimestamp:
          type: integer
          format: int64
          description: Request events newer than this Unix timestamp (ms)
          example: 1733640000000
        targetClientId:
          type: string
          description: Optionally request from a specific peer only
          example: "device-def-789"

    PullResponseMessage:
      allOf:
        - $ref: '#/components/schemas/LiveStoreMessageBase'
        - type: object
          properties:
            type:
              type: string
              const: pull_response
            payload:
              $ref: '#/components/schemas/PullResponsePayload'
            onlinePeers:
              type: array
              description: List of online peer client IDs that can provide history
              items:
                type: string
              example: ["device-def-789", "device-ghi-012"]

    PullResponsePayload:
      type: object
      description: Payload for pull response messages
      required:
        - events
        - fromClientId
        - hasMore
      properties:
        events:
          type: array
          description: Events matching the pull request
          items:
            $ref: '#/components/schemas/LiveStoreEvent'
        fromClientId:
          type: string
          description: Client ID that provided these events
          example: "server"
        hasMore:
          type: boolean
          description: Whether more events are available for pagination
          example: false
        nextTimestamp:
          type: integer
          format: int64
          description: Cursor timestamp for fetching next page (if hasMore is true)
          example: 1733642000000

    AckMessage:
      allOf:
        - $ref: '#/components/schemas/LiveStoreMessageBase'
        - type: object
          properties:
            type:
              type: string
              const: ack
            payload:
              $ref: '#/components/schemas/AckPayload'

    AckPayload:
      type: object
      description: Payload for acknowledgment messages
      required:
        - receivedCount
        - lastTimestamp
      properties:
        receivedCount:
          type: integer
          format: int64
          description: Number of events received and processed
          example: 3
        lastTimestamp:
          type: integer
          format: int64
          description: Timestamp of the last processed event
          example: 1733644800000
        errors:
          type: array
          description: Any errors encountered processing events
          items:
            $ref: '#/components/schemas/SyncError'

    SyncError:
      type: object
      description: Error encountered during sync
      required:
        - code
        - message
      properties:
        code:
          type: string
          description: Error code
          example: "invalid_event"
        message:
          type: string
          description: Human-readable error message
          example: "Event payload missing required field"
        eventNumber:
          type: integer
          format: int64
          description: Event number that caused the error (if applicable)
          example: 42

    PeerPullMessage:
      allOf:
        - $ref: '#/components/schemas/LiveStoreMessageBase'
        - type: object
          properties:
            type:
              type: string
              const: peer_pull
            payload:
              $ref: '#/components/schemas/PeerPullPayload'

    PeerPullPayload:
      type: object
      description: Payload for peer-to-peer pull requests
      required:
        - requestId
        - sinceTimestamp
      properties:
        requestId:
          type: string
          description: Unique identifier for this request (for matching responses)
          example: "req-abc-123"
        sinceTimestamp:
          type: integer
          format: int64
          description: Request events newer than this Unix timestamp (ms)
          example: 1733640000000
        retentionDays:
          type: integer
          description: Limit history to this many days (optional)
          example: 30

    PeerPullResponseMessage:
      allOf:
        - $ref: '#/components/schemas/LiveStoreMessageBase'
        - type: object
          properties:
            type:
              type: string
              const: peer_pull_response
            payload:
              $ref: '#/components/schemas/PeerPullResponsePayload'

    PeerPullResponsePayload:
      type: object
      description: Payload for peer-to-peer pull response
      required:
        - requestId
        - events
        - hasMore
      properties:
        requestId:
          type: string
          description: Matches the requestId from the peer_pull request
          example: "req-abc-123"
        events:
          type: array
          description: Historical events from this peer
          items:
            $ref: '#/components/schemas/LiveStoreEvent'
        hasMore:
          type: boolean
          description: Whether more events are available
          example: false
        nextTimestamp:
          type: integer
          format: int64
          description: Cursor timestamp for pagination (if hasMore is true)
          example: 1733642000000

    ErrorMessage:
      type: object
      description: Error message from server
      required:
        - type
        - code
        - message
      properties:
        type:
          type: string
          const: error
        code:
          type: string
          description: Error code identifier
          enum:
            - invalid_message
            - invalid_payload
            - unknown_type
            - internal_error
          example: "invalid_message"
        message:
          type: string
          description: Human-readable error description
          example: "Failed to parse message"
