openapi: 3.0.3
info:
  title: Training Pipes API
  version: "1.0.0"
servers:
  - url: https://api.trainingpipes.com
  - url: http://localhost:8080
tags:
  - name: Config
  - name: Projects
  - name: Users
  - name: Connections
  - name: File Systems
  - name: Gateways
  - name: Usage
  - name: Buckets
  - name: Health
  - name: Public File Systems
  - name: Webhooks
  - name: ApiKeys
  - name: Plans
  - name: Billing
  - name: Subscriptions

paths:
  /v1/config:
    get:
      tags: [Config]
      operationId: getConfig
      summary: Get platform configuration
      description: Returns available providers and their supported regions. This is a public endpoint.
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: {$ref: '#/components/schemas/PlatformConfig'}

  /v1/health:
    get:
      tags: [Health]
      operationId: healthCheck
      summary: Liveness probe
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: {$ref: '#/components/schemas/Health'}

  /v1/projects:
    post:
      tags: [Projects]
      operationId: createProject
      summary: Create project (stubbed)
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/ProjectCreate'}
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema: {$ref: '#/components/schemas/Project'}
    get:
      tags: [Projects]
      operationId: listProjects
      summary: List projects (stubbed)
      parameters:
        - in: query
          name: page_size
          schema: {type: integer, minimum: 1, maximum: 1000, default: 50}
        - in: query
          name: page_token
          schema: {type: string}
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: {$ref: '#/components/schemas/ProjectList'}

  /v1/projects/{project_id}:
    parameters:
      - in: path
        name: project_id
        required: true
        schema: {type: string}
    get:
      tags: [Projects]
      operationId: getProject
      summary: Get project (stubbed)
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: {$ref: '#/components/schemas/Project'}
        "404": {description: Not found}
    patch:
      tags: [Projects]
      operationId: updateProject
      summary: Update project (stubbed)
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/ProjectUpdate'}
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema: {$ref: '#/components/schemas/Project'}
    delete:
      tags: [Projects]
      operationId: deleteProject
      summary: Delete project (stubbed)
      responses:
        "204": {description: Deleted}

  /v1/users/{user_id}:
    parameters:
      - in: path
        name: user_id
        required: true
        schema: {type: string}
    get:
      tags: [Users]
      operationId: getUser
      summary: Get user profile
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: {$ref: '#/components/schemas/User'}
        "404": {description: Not found}
    patch:
      tags: [Users]
      operationId: updateUser
      summary: Update user profile
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/UserUpdate'}
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema: {$ref: '#/components/schemas/User'}

  /v1/users/{user_id}/project-memberships:
    parameters:
      - in: path
        name: user_id
        required: true
        schema: {type: string}
    get:
      tags: [Projects]
      operationId: listUserProjectMemberships
      summary: List project memberships for a user
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: {$ref: '#/components/schemas/ProjectMembershipList'}

  /v1/projects/{project_id}/memberships:
    parameters:
      - in: path
        name: project_id
        required: true
        schema: {type: string}
    get:
      tags: [Projects]
      operationId: listProjectMemberships
      summary: List all memberships (team members) for a project
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: {$ref: '#/components/schemas/ProjectMembershipList'}

  /v1/projects/{project_id}/connections:
    parameters:
      - in: path
        name: project_id
        required: true
        schema: {type: string}
    post:
      tags: [Connections]
      operationId: createConnection
      summary: Create BYO storage connection (stubbed)
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/ConnectionCreate'}
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema: {$ref: '#/components/schemas/Connection'}
    get:
      tags: [Connections]
      operationId: listConnections
      summary: List connections (stubbed)
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: {$ref: '#/components/schemas/ConnectionList'}

  /v1/connections/{conn_id}:
    parameters:
      - in: path
        name: conn_id
        required: true
        schema: {type: string}
    get:
      tags: [Connections]
      operationId: getConnection
      summary: Get connection (stubbed)
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: {$ref: '#/components/schemas/Connection'}
        "404": {description: Not found}
    delete:
      tags: [Connections]
      operationId: deleteConnection
      summary: Delete a connection
      responses:
        "204": {description: Connection deleted}
        "404": {description: Not found}

  /v1/connections/{conn_id}:verify:
    parameters:
      - in: path
        name: conn_id
        required: true
        schema: {type: string}
    post:
      tags: [Connections]
      operationId: verifyConnection
      summary: Verify connection (stubbed)
      responses:
        "200":
          description: Verification result
          content:
            application/json:
              schema: {$ref: '#/components/schemas/VerifyResult'}

  /v1/projects/{project_id}/buckets:
    parameters:
      - in: path
        name: project_id
        required: true
        schema: {type: string}
    post:
      tags: [Buckets]
      operationId: createBucket
      summary: Create a managed storage bucket
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/BucketCreate'}
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema: {$ref: '#/components/schemas/Bucket'}
    get:
      tags: [Buckets]
      operationId: listBuckets
      summary: List managed buckets for a project
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: {$ref: '#/components/schemas/BucketList'}

  /v1/buckets/{bucket_id}:
    parameters:
      - in: path
        name: bucket_id
        required: true
        schema: {type: string}
    get:
      tags: [Buckets]
      operationId: getBucket
      summary: Get bucket details
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: {$ref: '#/components/schemas/Bucket'}
        "404": {description: Not found}
    delete:
      tags: [Buckets]
      operationId: deleteBucket
      summary: Delete a managed bucket
      responses:
        "204": {description: Deleted}
        "404": {description: Not found}

  /v1/buckets/{bucket_id}/objects:
    parameters:
      - in: path
        name: bucket_id
        required: true
        schema: {type: string}
    get:
      tags: [Buckets]
      operationId: listBucketObjects
      summary: List objects in a managed bucket
      description: Lists objects (files and folder prefixes) using S3 ListObjectsV2 with `/` as the delimiter. Supply a `prefix` to list the contents of a specific folder.
      parameters:
        - in: query
          name: prefix
          schema: {type: string, default: ""}
        - in: query
          name: continuation_token
          schema: {type: string}
        - in: query
          name: max_keys
          schema: {type: integer, minimum: 1, maximum: 1000, default: 200}
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: {$ref: '#/components/schemas/ObjectList'}
        "404": {description: Not found}
    put:
      tags: [Buckets]
      operationId: uploadBucketObject
      summary: Upload an object to a managed bucket
      description: Uploads the request body as an object at the given `key`. Streams directly to S3.
      parameters:
        - in: query
          name: key
          required: true
          schema: {type: string}
      requestBody:
        required: true
        content:
          application/octet-stream:
            schema: {type: string, format: binary}
      responses:
        "204": {description: Uploaded}
        "400": {description: Bad request}
        "404": {description: Not found}
    delete:
      tags: [Buckets]
      operationId: deleteBucketObject
      summary: Delete an object from a managed bucket
      description: Deletes the object at `key`. When `recursive=true`, `key` is treated as a folder prefix and every object beneath it is deleted.
      parameters:
        - in: query
          name: key
          required: true
          schema: {type: string}
        - in: query
          name: recursive
          schema: {type: boolean, default: false}
      responses:
        "204": {description: Deleted}
        "400": {description: Bad request}
        "404": {description: Not found}

  /v1/buckets/{bucket_id}/objects/download:
    parameters:
      - in: path
        name: bucket_id
        required: true
        schema: {type: string}
    get:
      tags: [Buckets]
      operationId: downloadBucketObject
      summary: Download an object from a managed bucket
      description: Streams the object body for the given `key` with a Content-Disposition attachment header. When `recursive=true`, `key` is treated as a folder prefix and a zip archive of everything beneath it is streamed.
      parameters:
        - in: query
          name: key
          required: true
          schema: {type: string}
        - in: query
          name: recursive
          schema: {type: boolean, default: false}
      responses:
        "200":
          description: Object content
          content:
            application/octet-stream:
              schema: {type: string, format: binary}
        "400": {description: Bad request}
        "404": {description: Not found}

  /v1/connections/{conn_id}/objects:
    parameters:
      - in: path
        name: conn_id
        required: true
        schema: {type: string}
    get:
      tags: [Connections]
      operationId: listConnectionObjects
      summary: List objects in a connected bucket
      description: Lists objects (files and folder prefixes) using S3 ListObjectsV2 with `/` as the delimiter. Supply a `prefix` to list the contents of a specific folder.
      parameters:
        - in: query
          name: prefix
          schema: {type: string, default: ""}
        - in: query
          name: continuation_token
          schema: {type: string}
        - in: query
          name: max_keys
          schema: {type: integer, minimum: 1, maximum: 1000, default: 200}
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: {$ref: '#/components/schemas/ObjectList'}
        "404": {description: Not found}
    put:
      tags: [Connections]
      operationId: uploadConnectionObject
      summary: Upload an object to a connected bucket
      description: Uploads the request body as an object at the given `key`. Streams directly to S3.
      parameters:
        - in: query
          name: key
          required: true
          schema: {type: string}
      requestBody:
        required: true
        content:
          application/octet-stream:
            schema: {type: string, format: binary}
      responses:
        "204": {description: Uploaded}
        "400": {description: Bad request}
        "404": {description: Not found}
    delete:
      tags: [Connections]
      operationId: deleteConnectionObject
      summary: Delete an object from a connected bucket
      description: Deletes the object at `key`. When `recursive=true`, `key` is treated as a folder prefix and every object beneath it is deleted.
      parameters:
        - in: query
          name: key
          required: true
          schema: {type: string}
        - in: query
          name: recursive
          schema: {type: boolean, default: false}
      responses:
        "204": {description: Deleted}
        "400": {description: Bad request}
        "404": {description: Not found}

  /v1/connections/{conn_id}/objects/download:
    parameters:
      - in: path
        name: conn_id
        required: true
        schema: {type: string}
    get:
      tags: [Connections]
      operationId: downloadConnectionObject
      summary: Download an object from a connected bucket
      description: Streams the object body for the given `key` with a Content-Disposition attachment header. When `recursive=true`, `key` is treated as a folder prefix and a zip archive of everything beneath it is streamed.
      parameters:
        - in: query
          name: key
          required: true
          schema: {type: string}
        - in: query
          name: recursive
          schema: {type: boolean, default: false}
      responses:
        "200":
          description: Object content
          content:
            application/octet-stream:
              schema: {type: string, format: binary}
        "400": {description: Bad request}
        "404": {description: Not found}

  /v1/projects/{project_id}/file-systems:
    parameters:
      - in: path
        name: project_id
        required: true
        schema: {type: string}
    post:
      tags: [File Systems]
      operationId: createFileSystem
      summary: Create file system (stubbed)
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/FileSystemCreate'}
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema: {$ref: '#/components/schemas/FileSystem'}
    get:
      tags: [File Systems]
      operationId: listFileSystems
      summary: List file systems (stubbed)
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: {$ref: '#/components/schemas/FileSystemList'}

  /v1/file-systems:
    get:
      tags: [File Systems]
      operationId: listAllFileSystems
      summary: List all file systems across all tenants
      parameters:
        - in: query
          name: page_size
          schema: {type: integer, minimum: 1, maximum: 100, default: 20}
        - in: query
          name: page_token
          schema: {type: string}
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: {$ref: '#/components/schemas/FileSystemList'}

  /v1/file-systems/{file_system_id}:
    parameters:
      - in: path
        name: file_system_id
        required: true
        schema: {type: string}
    get:
      tags: [File Systems]
      operationId: getFileSystem
      summary: Get file system (stubbed)
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: {$ref: '#/components/schemas/FileSystem'}
        "404": {description: Not found}
    patch:
      tags: [File Systems]
      operationId: updateFileSystem
      summary: Update file system
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/FileSystemUpdate'}
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: {$ref: '#/components/schemas/FileSystem'}
        "404": {description: Not found}
    delete:
      tags: [File Systems]
      operationId: deleteFileSystem
      summary: Delete file system (stubbed)
      responses:
        "204": {description: Deleted}

  /v1/file-systems/{file_system_id}:flush:
    parameters:
      - in: path
        name: file_system_id
        required: true
        schema: {type: string}
    post:
      tags: [File Systems]
      operationId: flushFileSystem
      summary: Trigger flush (stubbed)
      responses:
        "202": {description: Flush started}

  /v1/file-systems/{file_system_id}/events:
    parameters:
      - in: path
        name: file_system_id
        required: true
        schema: {type: string}
    get:
      tags: [File Systems]
      operationId: streamFileSystemEvents
      summary: SSE stream (stubbed)
      responses:
        "200":
          description: text/event-stream
          content:
            text/event-stream:
              schema:
                type: string
                example: "event: status\ndata: {\"status\":\"ready\"}\n\n"

  /v1/gateways:
    post:
      tags: [Gateways]
      operationId: createGateway
      summary: Create gateway (stubbed)
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/GatewayCreate'}
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: {$ref: '#/components/schemas/Gateway'}

  /v1/gateways/{gateway_id}:attach:
    post:
      tags: [Gateways]
      operationId: attachFileSystem
      parameters:
        - in: path
          name: gateway_id
          required: true
          schema: {type: string}
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [file_system_id]
              properties:
                file_system_id: {type: string}
      responses:
        "204": {description: Attached}

  /v1/gateways/{gateway_id}:detach:
    post:
      tags: [Gateways]
      operationId: detachFileSystem
      parameters:
        - in: path
          name: gateway_id
          required: true
          schema: {type: string}
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [file_system_id]
              properties:
                file_system_id: {type: string}
      responses:
        "204": {description: Detached}

  /v1/projects/{project_id}/usage:
    get:
      tags: [Usage]
      operationId: getUsage
      summary: Get storage usage report for a project
      parameters:
        - in: path
          name: project_id
          required: true
          schema: {type: string}
        - in: query
          name: from
          schema: {type: string, format: date-time}
        - in: query
          name: to
          schema: {type: string, format: date-time}
        - in: query
          name: granularity
          schema: {type: string, enum: [hour, day], default: day}
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: {$ref: '#/components/schemas/UsageReport'}

  /v1/webhooks/stripe:
    post:
      tags: [Webhooks]
      operationId: handleStripeWebhook
      summary: Handle Stripe webhook events
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
      responses:
        "200":
          description: Webhook processed

  /v1/projects/{project_id}/portal_session:
    post:
      tags: [Projects]
      operationId: createProjectPortalSession
      summary: Create Stripe billing portal session for project
      parameters:
        - in: path
          name: project_id
          required: true
          schema: {type: string}
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/PortalSessionCreate'}
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: {$ref: '#/components/schemas/PortalSession'}
        "400":
          description: Bad request
        "404":
          description: Project not found

  /v1/projects/{project_id}/subscription:
    get:
      tags: [Projects]
      operationId: getProjectSubscription
      summary: Get the current Stripe subscription state for a project
      parameters:
        - in: path
          name: project_id
          required: true
          schema: {type: string}
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: {$ref: '#/components/schemas/ProjectSubscription'}
        "404":
          description: Project not found

  /v1/projects/{project_id}/invoices:
    get:
      tags: [Projects]
      operationId: listProjectInvoices
      summary: List Stripe invoices for the project's customer
      description: |
        Returns invoices on the project's Stripe customer, most recent first.
        Includes both base-plan and metered (sibling) subscription invoices
        since they share a customer.
      parameters:
        - in: path
          name: project_id
          required: true
          schema: {type: string}
        - in: query
          name: limit
          schema: {type: integer, default: 24, minimum: 1, maximum: 100}
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: {$ref: '#/components/schemas/InvoiceList'}
        "404":
          description: Project not found

  /v1/projects/{project_id}/upcoming_invoice:
    get:
      tags: [Projects]
      operationId: getProjectUpcomingInvoice
      summary: Preview the next invoice the project will be charged
      description: |
        Returns Stripe's preview of the next invoice for the project's
        active subscription, including projected metered usage. Returns 204
        when the project has no active subscription (and therefore no upcoming
        invoice).
      parameters:
        - in: path
          name: project_id
          required: true
          schema: {type: string}
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: {$ref: '#/components/schemas/UpcomingInvoice'}
        "204":
          description: No active subscription
        "404":
          description: Project not found

  /v1/projects/{project_id}/subscription/cancel:
    post:
      tags: [Projects]
      operationId: cancelProjectSubscription
      summary: Schedule cancellation of the project's subscription at period end
      description: |
        Marks the project's base Stripe subscription for cancellation at the
        end of the current billing period. The customer keeps access until
        then. Use `resumeProjectSubscription` to undo before the period ends.
      parameters:
        - in: path
          name: project_id
          required: true
          schema: {type: string}
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: {$ref: '#/components/schemas/ProjectSubscription'}
        "404":
          description: Project not found
        "409":
          description: Project has no active subscription to cancel

  /v1/projects/{project_id}/subscription/resume:
    post:
      tags: [Projects]
      operationId: resumeProjectSubscription
      summary: Undo a scheduled cancellation of the project's subscription
      description: |
        Clears the `cancel_at_period_end` flag on the project's base Stripe
        subscription so it continues renewing.
      parameters:
        - in: path
          name: project_id
          required: true
          schema: {type: string}
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: {$ref: '#/components/schemas/ProjectSubscription'}
        "404":
          description: Project not found
        "409":
          description: Project has no active subscription to resume

  /v1/projects/{project_id}/plan:
    post:
      tags: [Projects]
      operationId: changeProjectPlan
      summary: Change the Stripe plan for an existing subscription
      description: |
        Swaps the base plan price of the project's existing Stripe subscription.
        Use this for upgrades/downgrades from an existing paying state. For
        brand-new subscriptions, use `createProjectCheckoutSession` instead.
      parameters:
        - in: path
          name: project_id
          required: true
          schema: {type: string}
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/ChangePlanRequest'}
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: {$ref: '#/components/schemas/ProjectSubscription'}
        "400":
          description: Bad request
        "404":
          description: Project not found
        "409":
          description: Project has no active subscription to change

  /v1/projects/{project_id}/checkout_session:
    post:
      tags: [Projects]
      operationId: createProjectCheckoutSession
      summary: Create Stripe checkout session for project
      parameters:
        - in: path
          name: project_id
          required: true
          schema: {type: string}
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/CheckoutSessionCreate'}
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: {$ref: '#/components/schemas/CheckoutSession'}
        "400":
          description: Bad request
        "404":
          description: Project not found

  /v1/plans:
    get:
      tags: [Projects]
      operationId: listPlans
      summary: List all subscription plans with features (public)
      description: Returns all available subscription plans with their associated features. This is a public endpoint that does not require authentication.
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: {$ref: '#/components/schemas/PlanList'}

  /v1/subscription-features:
    post:
      tags: [Projects]
      operationId: createSubscriptionFeature
      summary: Create subscription feature
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/SubscriptionFeatureCreate'}
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema: {$ref: '#/components/schemas/SubscriptionFeature'}

  /v1/subscription-features/{id}:
    parameters:
      - in: path
        name: id
        required: true
        schema: {type: string}
    patch:
      tags: [Projects]
      operationId: updateSubscriptionFeature
      summary: Update subscription feature
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/SubscriptionFeatureUpdate'}
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema: {$ref: '#/components/schemas/SubscriptionFeature'}

  /v1/subscription-plans:
    post:
      tags: [Projects]
      operationId: createSubscriptionPlan
      summary: Create subscription plan
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/SubscriptionPlanCreate'}
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema: {$ref: '#/components/schemas/SubscriptionPlan'}

  /v1/subscription-plans/{id}:
    parameters:
      - in: path
        name: id
        required: true
        schema: {type: string}
    patch:
      tags: [Projects]
      operationId: updateSubscriptionPlan
      summary: Update subscription plan
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/SubscriptionPlanUpdate'}
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema: {$ref: '#/components/schemas/SubscriptionPlan'}

  /v1/subscription-plan-features:
    post:
      tags: [Projects]
      operationId: createSubscriptionPlanFeature
      summary: Create subscription plan feature
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/SubscriptionPlanFeatureCreate'}
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema: {$ref: '#/components/schemas/SubscriptionPlanFeature'}

  /v1/subscription-plan-features/{id}:
    parameters:
      - in: path
        name: id
        required: true
        schema: {type: string}
    patch:
      tags: [Projects]
      operationId: updateSubscriptionPlanFeature
      summary: Update subscription plan feature
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/SubscriptionPlanFeatureUpdate'}
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema: {$ref: '#/components/schemas/SubscriptionPlanFeature'}

  /v1/subscription-plan-feature-usages:
    post:
      tags: [Projects]
      operationId: createSubscriptionPlanFeatureUsage
      summary: Create subscription plan feature usage
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/SubscriptionPlanFeatureUsageCreate'}
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema: {$ref: '#/components/schemas/SubscriptionPlanFeatureUsage'}

  /v1/subscription-plan-feature-usages/{id}:
    parameters:
      - in: path
        name: id
        required: true
        schema: {type: string}
    patch:
      tags: [Projects]
      operationId: updateSubscriptionPlanFeatureUsage
      summary: Update subscription plan feature usage
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/SubscriptionPlanFeatureUsageUpdate'}
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema: {$ref: '#/components/schemas/SubscriptionPlanFeatureUsage'}

  /v1/api-keys:
    get:
      tags: [ApiKeys]
      operationId: listApiKeys
      summary: List API keys for the authenticated user
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: {$ref: '#/components/schemas/ApiKeyList'}
    post:
      tags: [ApiKeys]
      operationId: createApiKey
      summary: Create a new API key
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/ApiKeyCreate'}
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema: {$ref: '#/components/schemas/ApiKeyCreated'}

  /v1/api-keys/{api_key_id}:
    parameters:
      - in: path
        name: api_key_id
        required: true
        schema: {type: string}
    delete:
      tags: [ApiKeys]
      operationId: deleteApiKey
      summary: Revoke an API key
      responses:
        "204": {description: Deleted}
        "404": {description: Not found}

  /v1/public-file-systems:
    get:
      tags: [Public File Systems]
      operationId: listPublicFileSystems
      summary: List publicly available example file systems
      description: Returns file systems available for public demo use. No authentication required.
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: {$ref: '#/components/schemas/PublicFileSystemList'}

components:
  schemas:
    Health:
      type: object
      properties:
        status: {type: string, enum: [ok]}
        version: {type: string}

    ProjectCreate:
      type: object
      required: [name]
      properties:
        name: {type: string}

    ProjectUpdate:
      type: object
      properties:
        name: {type: string}

    Project:
      type: object
      properties:
        id: {type: string}
        name: {type: string}
        creator_id: {type: string}
        stripe_customer_id: {type: string}
        stripe_subscription_id: {type: string}
        stripe_price_id:
          type: string
          description: Current Stripe price ID of the subscription's base plan
        stripe_subscription_status:
          type: string
          description: Latest known status of the Stripe subscription (active, past_due, canceled, ...)
        created_at: {type: string, format: date-time}
        updated_at: {type: string, format: date-time}

    ProjectList:
      type: object
      properties:
        items:
          type: array
          items: {$ref: '#/components/schemas/Project'}
        next_page_token: {type: string}

    ProjectMembership:
      type: object
      properties:
        id: {type: string}
        user_id: {type: string}
        project_id: {type: string}
        project: {$ref: '#/components/schemas/Project'}
        user: {$ref: '#/components/schemas/User'}
        role: {type: string, enum: [owner, admin, member]}
        status: {type: string, enum: [invited, accepted, denied]}
        invitation_sent_at: {type: string, format: date-time}
        invitation_accepted_at: {type: string, format: date-time}
        invitation_denied_at: {type: string, format: date-time}
        created_at: {type: string, format: date-time}

    ProjectMembershipList:
      type: object
      properties:
        items:
          type: array
          items: {$ref: '#/components/schemas/ProjectMembership'}

    S3Spec:
      type: object
      properties:
        bucket_name: {type: string}
        region: {type: string}
        endpoint: {type: string, description: "Custom endpoint for S3-compatible storage (e.g., R2, MinIO)"}
        access_key_id: {type: string}
        secret_access_key: {type: string}

    GcpSpec:
      type: object
      properties:
        bucket_name: {type: string}
        region: {type: string}
        service_account_json: {type: string}

    ConnectionCreate:
      type: object
      required: [provider, uri, region, access_key_id, secret_access_key]
      properties:
        provider: {type: string, enum: [aws, gcp, s3compat]}
        uri: {type: string, example: "s3://bucket/prefix"}
        region: {type: string, example: "aws:us-east-1"}
        s3_spec: {$ref: '#/components/schemas/S3Spec'}
        gcp_spec: {$ref: '#/components/schemas/GcpSpec'}

    Connection:
      type: object
      properties:
        id: {type: string}
        project_id: {type: string}
        provider: {type: string}
        uri: {type: string}
        region: {type: string}
        status: {type: string, enum: [pending, verifying, verified, invalid, failed, scaling, running, stopped, terminated]}
        desired_status: {type: string, enum: [running, stopped]}
        s3_spec: {$ref: '#/components/schemas/S3Spec'}
        gcp_spec: {$ref: '#/components/schemas/GcpSpec'}
        created_at: {type: string, format: date-time}
        updated_at: {type: string, format: date-time}

    ConnectionList:
      type: object
      properties:
        items:
          type: array
          items: {$ref: '#/components/schemas/Connection'}

    VerifyResult:
      type: object
      properties:
        status: {type: string, enum: [verified, failed]}
        checks:
          type: array
          items:
            type: object
            properties:
              name: {type: string}
              ok: {type: boolean}

    FileSystemCreate:
      type: object
      required: [region]
      properties:
        connection_id: {type: string, description: "ID of a BYO connection (provide either connection_id or bucket_id)"}
        bucket_id: {type: string, description: "ID of a managed bucket (provide either connection_id or bucket_id)"}
        region: {type: string}
        preload: {type: boolean, default: false}
        instance_type:
          type: string
          description: |
            EC2 instance type for the gateway that backs this file system. Must be one of the
            values returned by `/v1/config` under `aws_instance_types`. Free-plan projects must
            use the instance type marked `free_tier: true`. Defaults to the free-tier instance
            when omitted.

    FileSystemUpdate:
      type: object
      properties:
        desired_status: {type: string, enum: [running, stopped]}

    FileSystem:
      type: object
      properties:
        id: {type: string}
        project_id: {type: string}
        connection_id: {type: string}
        bucket_id: {type: string}
        region: {type: string}
        instance_type: {type: string, description: "EC2 instance type for the gateway backing this file system"}
        status: {type: string, enum: [pending, running, scaling, terminating, stopped, failed]}
        desired_status: {type: string, enum: [running, stopped]}
        export: {$ref: '#/components/schemas/FileSystemExport'}
        created_at: {type: string, format: date-time}

    FileSystemExport:
      type: object
      properties:
        nfs: {$ref: '#/components/schemas/FileSystemExportNfs'}
        smb: {$ref: '#/components/schemas/FileSystemExportSmb'}
        agent: {$ref: '#/components/schemas/FileSystemExportAgent'}
        wireguard: {$ref: '#/components/schemas/FileSystemExportWireguard'}

    FileSystemExportNfs:
      type: object
      properties:
        server: {type: string}
        port: {type: integer}
        path: {type: string}

    FileSystemExportSmb:
      type: object
      properties:
        server: {type: string}
        port: {type: integer}
        share: {type: string}

    FileSystemExportAgent:
      type: object
      properties:
        download_url: {type: string}

    FileSystemExportWireguard:
      type: object
      properties:
        server_public_key: {type: string}
        endpoint: {type: string}
        client_private_key: {type: string}
        client_ip: {type: string}

    FileSystemList:
      type: object
      properties:
        items:
          type: array
          items: {$ref: '#/components/schemas/FileSystem'}
        next_page_token: {type: string}

    GatewayCreate:
      type: object
      required: [project_id, provider, region, type]
      properties:
        project_id: {type: string}
        provider: {type: string, enum: [aws, gcp, local]}
        region: {type: string}
        type: {type: string}

    Gateway:
      type: object
      properties:
        id: {type: string}
        project_id: {type: string}
        provider: {type: string, enum: [aws, gcp, local]}
        region: {type: string}
        type: {type: string}
        replicas: {type: integer}
        status: {type: string, enum: [pending, running, scaling, terminating, stopped, failed]}
        desired_status: {type: string, enum: [running, stopped]}

    UsageReport:
      type: object
      description: |
        Per-project usage and cost report.

        The "current state" fields (`current_total_bytes`, `buckets`,
        `total_file_system_hours`, `file_systems`, `daily_usage`) are kept
        for backward compatibility with the MCP `get_usage` tool and the
        SDK. The "cost explorer" fields (`currency`, `cost_days`,
        `line_items`) drive the dashboard's billing/usage page.
      properties:
        project_id: {type: string}
        from: {type: string, format: date-time}
        to: {type: string, format: date-time}
        currency:
          type: string
          description: ISO-4217 currency code, lowercased (e.g. "usd"). All cost fields are integer minor units of this currency.
        current_total_bytes: {type: integer, format: int64}
        buckets:
          type: array
          items:
            $ref: '#/components/schemas/BucketUsageSummary'
        daily_usage:
          type: array
          items:
            $ref: '#/components/schemas/DailyUsage'
        total_file_system_hours:
          type: number
          format: double
          description: Total file-system-hours across all running file systems
        file_systems:
          type: array
          items:
            $ref: '#/components/schemas/FileSystemUsageSummary'
        cost_days:
          type: array
          description: Per-day cost roll-up across the requested range, one entry per UTC day (zero-cost days included so the X axis stays continuous).
          items:
            $ref: '#/components/schemas/UsageCostDay'
        line_items:
          type: array
          description: Every individual line item that drove `cost_days`. Useful for tables and CSV export.
          items:
            $ref: '#/components/schemas/UsageLineItem'

    BucketUsageSummary:
      type: object
      properties:
        bucket_id: {type: string}
        bucket_name: {type: string}
        current_size_bytes: {type: integer, format: int64}
        object_count: {type: integer}

    DailyUsage:
      type: object
      properties:
        date: {type: string}
        avg_bytes: {type: integer, format: int64}

    FileSystemUsageSummary:
      type: object
      properties:
        file_system_id: {type: string}
        region: {type: string}
        status: {type: string}
        running_hours:
          type: number
          format: double
          description: Hours this file system has been running in the current session

    UsageCostDay:
      type: object
      description: Cost roll-up for a single UTC day.
      properties:
        date:
          type: string
          description: UTC date in YYYY-MM-DD format.
        storage_cost_cents:
          type: integer
          format: int64
          description: Total storage cost on this day, in minor currency units.
        filesystem_cost_cents:
          type: integer
          format: int64
          description: Total file system (compute) cost on this day, in minor currency units.

    UsageLineItem:
      type: object
      description: A single contributing line item to the cost report.
      properties:
        date:
          type: string
          description: UTC date in YYYY-MM-DD format.
        kind:
          type: string
          enum: [storage, filesystem]
          description: Cost category — storage (per-bucket) or filesystem (per-FS compute).
        resource_id:
          type: string
          description: Bucket id (for storage) or file system id (for filesystem).
        resource_label:
          type: string
          description: Human-readable label, e.g. bucket name, or "<region>/<instance-type>".
        quantity:
          type: number
          format: double
          description: Number of units of `unit` consumed.
        unit:
          type: string
          description: 'Unit the quantity is measured in (e.g. "GB-day", "instance-hour").'
        unit_price_cents:
          type: integer
          format: int64
          description: Price per unit in minor currency units. Approximate retail estimate; not the source of truth — Stripe holds the canonical price list.
        cost_cents:
          type: integer
          format: int64
          description: Total cost for this line in minor currency units (`quantity * unit_price_cents`, rounded to the nearest whole cent).

    User:
      type: object
      properties:
        id: {type: string}
        email: {type: string}
        first_name: {type: string}
        last_name: {type: string}
        created_at: {type: string, format: date-time}
        updated_at: {type: string, format: date-time}

    UserUpdate:
      type: object
      properties:
        first_name: {type: string}
        last_name: {type: string}

    PortalSessionCreate:
      type: object
      required: [return_url]
      properties:
        return_url:
          type: string
          format: uri
          description: URL to redirect to after the portal session

    PortalSession:
      type: object
      properties:
        url:
          type: string
          format: uri
          description: URL to redirect the user to for the Stripe billing portal

    CheckoutSessionCreate:
      type: object
      required: [price_id, success_url, cancel_url]
      properties:
        price_id:
          type: string
          description: Stripe price ID for the product/subscription
        success_url:
          type: string
          format: uri
          description: URL to redirect to after successful payment
        cancel_url:
          type: string
          format: uri
          description: URL to redirect to if payment is cancelled
        mode:
          type: string
          enum: [payment, subscription, setup]
          default: subscription
          description: Checkout session mode

    CheckoutSession:
      type: object
      properties:
        url:
          type: string
          format: uri
          description: URL to redirect the user to for the Stripe checkout
        session_id:
          type: string
          description: Stripe checkout session ID

    ChangePlanRequest:
      type: object
      required: [price_id]
      properties:
        price_id:
          type: string
          description: Stripe price ID to switch the subscription's base plan to

    ProjectSubscription:
      type: object
      properties:
        subscription_id:
          type: string
          description: Stripe subscription ID, empty if no subscription exists
        status:
          type: string
          description: Stripe subscription status (active, trialing, past_due, canceled, ...)
        price_id:
          type: string
          description: Stripe price ID of the current base plan
        cancel_at_period_end:
          type: boolean
          description: True if the subscription is scheduled to cancel at the end of the current billing period
        current_period_end:
          type: integer
          format: int64
          description: Unix timestamp (seconds) when the current billing period ends, 0 if not applicable
        current_period_start:
          type: integer
          format: int64
          description: Unix timestamp (seconds) when the current billing period started, 0 if not applicable

    Invoice:
      type: object
      description: |
        A single Stripe invoice for the project's customer. `reason` is a
        derived label intended for direct display: it folds together Stripe's
        `billing_reason` and whether the invoice has metered/usage line items,
        because customers care more about "what am I being charged for?" than
        about Stripe's raw event taxonomy.
      properties:
        id: {type: string}
        number:
          type: string
          description: Stripe's human-readable invoice number (e.g. ABCD-0001)
        status:
          type: string
          description: Stripe invoice status (paid, open, void, uncollectible, draft)
        amount_due_cents:
          type: integer
          format: int64
          description: Amount owed in the smallest currency unit (cents for USD)
        amount_paid_cents:
          type: integer
          format: int64
        currency:
          type: string
          description: ISO 4217 currency code, lowercase (e.g. "usd")
        created:
          type: integer
          format: int64
          description: Unix timestamp (seconds) the invoice was created
        period_start:
          type: integer
          format: int64
        period_end:
          type: integer
          format: int64
        hosted_invoice_url:
          type: string
          description: Stripe-hosted invoice page (logged-in customer view)
        invoice_pdf:
          type: string
          description: Direct PDF download URL for the invoice
        reason:
          type: string
          description: |
            Display label for the invoice: one of `subscription_create` (new
            subscription), `subscription_update` (plan change / proration),
            `subscription_cycle` (recurring renewal), `subscription_usage`
            (recurring renewal that includes metered usage), or `manual`.
        billing_reason:
          type: string
          description: Raw Stripe billing_reason (subscription_create, subscription_cycle, subscription_update, manual, ...)

    InvoiceList:
      type: object
      properties:
        items:
          type: array
          items: {$ref: '#/components/schemas/Invoice'}
        has_more:
          type: boolean
          description: True if more invoices exist beyond this page

    UpcomingInvoice:
      type: object
      description: |
        Preview of the next invoice Stripe will generate at period end. Useful
        for surfacing "you'll be billed X on Y" on the billing page. May be
        null when the customer has no active subscription.
      properties:
        amount_due_cents:
          type: integer
          format: int64
        currency:
          type: string
        period_start:
          type: integer
          format: int64
        period_end:
          type: integer
          format: int64
        next_payment_attempt:
          type: integer
          format: int64
          description: Unix timestamp (seconds) of the next charge attempt, 0 if unknown

    SubscriptionFeatureCreate:
      type: object
      required: [name]
      properties:
        name: {type: string}
        is_binary_based: {type: boolean}
        is_usage_based: {type: boolean}
        usage_unit: {type: string}
        highlight: {type: boolean}

    SubscriptionFeatureUpdate:
      type: object
      properties:
        name: {type: string}
        is_binary_based: {type: boolean}
        is_usage_based: {type: boolean}
        usage_unit: {type: string}
        highlight: {type: boolean}

    SubscriptionFeature:
      type: object
      properties:
        id: {type: string}
        name: {type: string}
        is_binary_based: {type: boolean}
        is_usage_based: {type: boolean}
        usage_unit: {type: string}
        highlight: {type: boolean}
        created_at: {type: string, format: date-time}
        updated_at: {type: string, format: date-time}

    SubscriptionPlanCreate:
      type: object
      required: [name, stripe_product_id, monthly_price_id, yearly_price_id]
      properties:
        name: {type: string}
        stripe_product_id: {type: string}
        monthly_price_id: {type: string}
        yearly_price_id: {type: string}

    SubscriptionPlanUpdate:
      type: object
      properties:
        name: {type: string}
        stripe_product_id: {type: string}
        monthly_price_id: {type: string}
        yearly_price_id: {type: string}

    SubscriptionPlan:
      type: object
      properties:
        id: {type: string}
        name: {type: string}
        stripe_product_id: {type: string}
        monthly_price_id: {type: string}
        yearly_price_id: {type: string}
        created_at: {type: string, format: date-time}
        updated_at: {type: string, format: date-time}

    SubscriptionPlanFeatureCreate:
      type: object
      required: [subscription_plan_id, subscription_feature_id]
      properties:
        subscription_plan_id: {type: string}
        subscription_feature_id: {type: string}
        binary_value: {type: integer}
        usage_limit: {type: integer, format: int64}

    SubscriptionPlanFeatureUpdate:
      type: object
      properties:
        subscription_plan_id: {type: string}
        subscription_feature_id: {type: string}
        binary_value: {type: integer}
        usage_limit: {type: integer, format: int64}

    SubscriptionPlanFeature:
      type: object
      properties:
        id: {type: string}
        subscription_plan_id: {type: string}
        subscription_feature_id: {type: string}
        binary_value: {type: integer}
        usage_limit: {type: integer, format: int64}
        created_at: {type: string, format: date-time}
        updated_at: {type: string, format: date-time}

    SubscriptionPlanFeatureUsageCreate:
      type: object
      required: [subscription_feature_id, project_id, value]
      properties:
        subscription_feature_id: {type: string}
        project_id: {type: string}
        value: {type: integer}

    SubscriptionPlanFeatureUsageUpdate:
      type: object
      properties:
        subscription_feature_id: {type: string}
        project_id: {type: string}
        value: {type: integer}

    SubscriptionPlanFeatureUsage:
      type: object
      properties:
        id: {type: string}
        subscription_feature_id: {type: string}
        project_id: {type: string}
        value: {type: integer}
        created_at: {type: string, format: date-time}
        updated_at: {type: string, format: date-time}

    PlanFeature:
      type: object
      properties:
        feature_id: {type: string}
        feature_name: {type: string}
        is_binary_based: {type: boolean}
        is_usage_based: {type: boolean}
        usage_unit: {type: string}
        usage_limit: {type: integer, format: int64}
        binary_value: {type: integer}
        highlight: {type: boolean}

    PlanWithFeatures:
      type: object
      properties:
        id: {type: string}
        name: {type: string}
        stripe_product_id: {type: string}
        monthly_price_id: {type: string}
        yearly_price_id: {type: string}
        monthly_price: {type: integer, description: "Monthly price in cents (e.g., 4900 = $49.00)"}
        yearly_price: {type: integer, description: "Yearly price in cents (e.g., 49000 = $490.00)"}
        created_at: {type: string, format: date-time}
        updated_at: {type: string, format: date-time}
        features:
          type: array
          items: {$ref: '#/components/schemas/PlanFeature'}

    PlanList:
      type: object
      properties:
        items:
          type: array
          items: {$ref: '#/components/schemas/PlanWithFeatures'}

    PlatformConfig:
      type: object
      properties:
        providers:
          type: array
          items: {$ref: '#/components/schemas/ProviderConfig'}
        aws_instance_types:
          type: array
          description: |
            Ordered list of EC2 instance types the API exposes for AWS-backed
            file systems. The first entry is the default; the entry whose
            `free_tier` is true is the only option allowed for free-plan
            projects.
          items: {$ref: '#/components/schemas/AWSInstanceTypeOption'}

    AWSInstanceTypeOption:
      type: object
      properties:
        name: {type: string, description: "Instance type identifier (e.g. 't3.small')"}
        display_name: {type: string, description: "Short label shown in the UI dropdown"}
        description: {type: string, description: "One-line summary of vCPU/RAM/storage"}
        free_tier:
          type: boolean
          description: "True if free-plan projects are allowed to launch with this instance type"
        price_cents_per_hour:
          type: integer
          format: int64
          description: "Metered billing rate in USD cents per running instance-hour. 0 for free-tier instances."

    ProviderConfig:
      type: object
      properties:
        name:
          type: string
          description: Provider identifier (e.g., "aws", "gcp")
        display_name:
          type: string
          description: Human-readable provider name (e.g., "Amazon Web Services")
        regions:
          type: array
          items: {$ref: '#/components/schemas/RegionConfig'}

    RegionConfig:
      type: object
      properties:
        name:
          type: string
          description: Region identifier (e.g., "us-east-1")
        display_name:
          type: string
          description: Human-readable region name (e.g., "US East (N. Virginia)")

    ApiKeyCreate:
      type: object
      required: [name]
      properties:
        name:
          type: string
          description: A label for the API key (e.g., "CLI", "CI/CD")

    ApiKey:
      type: object
      properties:
        id: {type: string}
        name: {type: string}
        key_prefix:
          type: string
          description: First few characters of the key for identification
        last_used_at: {type: string, format: date-time}
        expires_at: {type: string, format: date-time}
        created_at: {type: string, format: date-time}

    ApiKeyCreated:
      type: object
      properties:
        id: {type: string}
        name: {type: string}
        key:
          type: string
          description: The full API key. Only shown once at creation time.
        key_prefix: {type: string}
        created_at: {type: string, format: date-time}

    ApiKeyList:
      type: object
      properties:
        items:
          type: array
          items: {$ref: '#/components/schemas/ApiKey'}

    PublicFileSystem:
      type: object
      properties:
        id: {type: string}
        name:
          type: string
          description: Human-readable name (e.g., "Example Dataset")
        description:
          type: string
          description: What this file system contains
        nfs_server: {type: string}
        nfs_path: {type: string}
        nfs_port:
          type: integer
          default: 2049
        region: {type: string}

    PublicFileSystemList:
      type: object
      properties:
        items:
          type: array
          items: {$ref: '#/components/schemas/PublicFileSystem'}

    BucketCreate:
      type: object
      required: [name, region]
      properties:
        name: {type: string, description: "User-friendly bucket name"}
        region: {type: string, description: "Storage region (e.g., us-east-1)"}

    Bucket:
      type: object
      properties:
        id: {type: string}
        project_id: {type: string}
        name: {type: string}
        bucket_name: {type: string, description: "Bucket name to use with the S3-compatible endpoint"}
        provider: {type: string, enum: [managed]}
        region: {type: string}
        endpoint: {type: string, description: "S3-compatible endpoint URL for accessing this bucket"}
        access_key_id: {type: string, description: "Access key for authenticating with the storage endpoint"}
        secret_access_key: {type: string, description: "Secret key for authenticating with the storage endpoint"}
        status: {type: string, enum: [pending, active, failed, deleting]}
        created_at: {type: string, format: date-time}
        updated_at: {type: string, format: date-time}

    BucketList:
      type: object
      properties:
        items:
          type: array
          items: {$ref: '#/components/schemas/Bucket'}

    ObjectEntry:
      type: object
      required: [key, name, type]
      properties:
        key:
          type: string
          description: Full object key (files) or prefix including trailing slash (folders).
        name:
          type: string
          description: Display name (last path segment).
        type:
          type: string
          enum: [file, folder]
        size:
          type: integer
          format: int64
          description: Size in bytes (files only).
        last_modified:
          type: string
          format: date-time
          description: Last modified timestamp (files only).

    ObjectList:
      type: object
      required: [items, prefix, has_more]
      properties:
        items:
          type: array
          items: {$ref: '#/components/schemas/ObjectEntry'}
        prefix:
          type: string
          description: The prefix that was listed.
        has_more:
          type: boolean
          description: Whether more results are available via continuation_token.
        next_continuation_token:
          type: string
