asyncapi: 3.0.0
info:
  title: Aoba Notification WebSocket API
  version: 1.0.0
  description: |
    Real-time push notifications via WebSocket for the Aoba messaging platform.

    ## Overview

    The notification WebSocket provides real-time push notifications for events that
    require immediate user attention, such as group invitations. This channel is
    **server-to-client only** - clients receive notifications but do not send messages.

    ## Connection

    - **Endpoint**: `/api/v1/notifications`
    - **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/notifications');
    ```

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

    ## Message Direction

    - **Server → Client**: All notification events (push only)
    - **Client → Server**: None (keepalive handled via WebSocket ping/pong frames)

    ## Connection Lifecycle

    1. Client connects with valid JWT authentication
    2. Server upgrades connection and registers user
    3. Server sends ping frames every 54 seconds to keep connection alive
    4. Client responds with pong frames (handled automatically by WebSocket)
    5. Server pushes notification events as they occur
    6. Connection closes on logout or network disconnect

    ## Keepalive Behavior

    | Parameter | Value | Description |
    |-----------|-------|-------------|
    | Write timeout | 10s | Time allowed to write a message |
    | Pong timeout | 60s | Time allowed for client to respond to ping |
    | Ping interval | 54s | Server sends ping frames at this interval |

    ## Multi-Device Support

    When a user connects from multiple devices:
    - Each device gets its own WebSocket connection
    - Notifications are broadcast to **all** connected devices for the user
    - Disconnecting one device does not affect others

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

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

channels:
  notifications:
    address: /api/v1/notifications
    title: Notification Channel
    description: |
      WebSocket channel for receiving real-time push notifications.

      This is a **receive-only** channel - clients subscribe to receive
      notifications but do not publish messages.
    messages:
      invitationReceived:
        $ref: '#/components/messages/InvitationReceived'
      invitationAccepted:
        $ref: '#/components/messages/InvitationAccepted'
      invitationDenied:
        $ref: '#/components/messages/InvitationDenied'
    servers:
      - $ref: '#/servers/production'
      - $ref: '#/servers/development'

operations:
  receiveNotification:
    action: receive
    channel:
      $ref: '#/channels/notifications'
    title: Receive Notification
    summary: Receive push notifications from the server
    description: |
      Subscribe to this channel to receive real-time notifications.

      Currently supported notification types:
      - `invitation_received` - Sent to invitee when invited to a group
      - `invitation_accepted` - Sent to inviter when their invitation is accepted
      - `invitation_denied` - Sent to inviter when their invitation is denied

      ## Usage Example

      ```javascript
      const ws = new WebSocket('wss://api.example.com/api/v1/notifications');

      ws.onmessage = (event) => {
        const notification = JSON.parse(event.data);

        switch (notification.type) {
          case 'invitation_received':
            showInvitationPopup(notification.data);
            break;
          case 'invitation_accepted':
            showSuccessToast(`${notification.data.acceptedByUsername} joined the group`);
            break;
          case 'invitation_denied':
            showInfoToast(`Invitation was declined`);
            break;
        }
      };

      ws.onclose = () => {
        // Implement reconnection logic
      };
      ```
    messages:
      - $ref: '#/channels/notifications/messages/invitationReceived'
      - $ref: '#/channels/notifications/messages/invitationAccepted'
      - $ref: '#/channels/notifications/messages/invitationDenied'

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:
    InvitationReceived:
      name: invitation_received
      title: Invitation Received
      summary: Notification when user receives a group invitation
      description: |
        Sent to the **invitee** when another user invites them to join a group.

        The invitee should display this notification to allow accepting or declining.
      contentType: application/json
      payload:
        $ref: '#/components/schemas/InvitationReceivedEvent'
      examples:
        - name: GroupInvitationExample
          summary: User receives invitation to join "Project Alpha" group
          payload:
            type: invitation_received
            timestamp: 1733644800000
            data:
              invitationId: "550e8400-e29b-41d4-a716-446655440000"
              groupId: "7c9e6679-7425-40de-944b-e07fc1f90ae7"
              groupName: "Project Alpha"
              inviterUsername: "alice"

    InvitationAccepted:
      name: invitation_accepted
      title: Invitation Accepted
      summary: Notification when an invitation is accepted
      description: |
        Sent to the **inviter** when the invitee accepts their group invitation.

        The inviter can update their UI to show the new member has joined.
      contentType: application/json
      payload:
        $ref: '#/components/schemas/InvitationAcceptedEvent'
      examples:
        - name: InvitationAcceptedExample
          summary: Bob accepts Alice's invitation to join "Project Alpha"
          payload:
            type: invitation_accepted
            timestamp: 1733645000000
            data:
              invitationId: "550e8400-e29b-41d4-a716-446655440000"
              groupId: "7c9e6679-7425-40de-944b-e07fc1f90ae7"
              groupName: "Project Alpha"
              acceptedByUsername: "bob"

    InvitationDenied:
      name: invitation_denied
      title: Invitation Denied
      summary: Notification when an invitation is denied
      description: |
        Sent to the **inviter** when the invitee declines their group invitation.

        The inviter can optionally show a notification or remove the pending
        invitation from their UI.
      contentType: application/json
      payload:
        $ref: '#/components/schemas/InvitationDeniedEvent'
      examples:
        - name: InvitationDeniedExample
          summary: Carol declines Alice's invitation
          payload:
            type: invitation_denied
            timestamp: 1733645200000
            data:
              invitationId: "660e8400-e29b-41d4-a716-446655440001"
              groupId: "7c9e6679-7425-40de-944b-e07fc1f90ae7"
              deniedByUsername: "carol"

  schemas:
    NotificationEventType:
      type: string
      description: The type of notification event
      enum:
        - invitation_received
        - invitation_accepted
        - invitation_denied
      x-enum-descriptions:
        invitation_received: Sent to invitee when a new invitation is created
        invitation_accepted: Sent to inviter when their invitation is accepted
        invitation_denied: Sent to inviter when their invitation is denied

    NotificationEventBase:
      type: object
      description: Base structure for all notification events
      required:
        - type
        - timestamp
        - data
      properties:
        type:
          $ref: '#/components/schemas/NotificationEventType'
        timestamp:
          type: integer
          format: int64
          description: Unix timestamp in milliseconds when the event was created
          example: 1733644800000
        data:
          type: object
          description: Event-specific data payload

    InvitationReceivedEvent:
      allOf:
        - $ref: '#/components/schemas/NotificationEventBase'
        - type: object
          properties:
            type:
              type: string
              const: invitation_received
            data:
              $ref: '#/components/schemas/InvitationReceivedData'

    InvitationAcceptedEvent:
      allOf:
        - $ref: '#/components/schemas/NotificationEventBase'
        - type: object
          properties:
            type:
              type: string
              const: invitation_accepted
            data:
              $ref: '#/components/schemas/InvitationAcceptedData'

    InvitationDeniedEvent:
      allOf:
        - $ref: '#/components/schemas/NotificationEventBase'
        - type: object
          properties:
            type:
              type: string
              const: invitation_denied
            data:
              $ref: '#/components/schemas/InvitationDeniedData'

    InvitationReceivedData:
      type: object
      description: Data payload for invitation_received notification
      required:
        - invitationId
        - groupId
        - groupName
        - inviterUsername
      properties:
        invitationId:
          type: string
          format: uuid
          description: Unique identifier of the invitation
          example: "550e8400-e29b-41d4-a716-446655440000"
        groupId:
          type: string
          format: uuid
          description: Unique identifier of the group
          example: "7c9e6679-7425-40de-944b-e07fc1f90ae7"
        groupName:
          type: string
          description: Display name of the group
          example: "Project Alpha"
        inviterUsername:
          type: string
          description: Username of the person who sent the invitation
          example: "alice"

    InvitationAcceptedData:
      type: object
      description: Data payload for invitation_accepted notification
      required:
        - invitationId
        - groupId
        - groupName
        - acceptedByUsername
      properties:
        invitationId:
          type: string
          format: uuid
          description: Unique identifier of the invitation that was accepted
          example: "550e8400-e29b-41d4-a716-446655440000"
        groupId:
          type: string
          format: uuid
          description: Unique identifier of the group
          example: "7c9e6679-7425-40de-944b-e07fc1f90ae7"
        groupName:
          type: string
          description: Display name of the group
          example: "Project Alpha"
        acceptedByUsername:
          type: string
          description: Username of the person who accepted the invitation
          example: "bob"

    InvitationDeniedData:
      type: object
      description: Data payload for invitation_denied notification
      required:
        - invitationId
        - groupId
        - deniedByUsername
      properties:
        invitationId:
          type: string
          format: uuid
          description: Unique identifier of the invitation that was denied
          example: "660e8400-e29b-41d4-a716-446655440001"
        groupId:
          type: string
          format: uuid
          description: Unique identifier of the group
          example: "7c9e6679-7425-40de-944b-e07fc1f90ae7"
        deniedByUsername:
          type: string
          description: Username of the person who denied the invitation
          example: "carol"
