openapi: 3.1.0
info:
  title: OpenCloud App Runtime API
  version: 0.2.2
  summary: Same-origin APIs available to applications deployed on OpenCloud.
  description: |
    Machine-readable reference for coding agents building an OpenCloud app.

    This specification covers the app-facing runtime only. It intentionally
    excludes the OpenCloud control plane, including `/v1/apps`, deployments,
    operations, credentials, secrets, backups, and administrative telemetry.

    Browser applications should use the deployment-pinned `@opencloud/js`
    module advertised by `GET /_opencloud/config`. The SDK owns app identity
    headers, bearer tokens, cookie forwarding, and session refresh. Do not
    copy the anonymous key or access tokens into application state.

    Database resources, row shapes, Function bodies, and Function responses
    are app-defined, so their schemas are necessarily open-ended here.
  contact:
    name: OpenCloud
    url: https://docs.opencloud.ai
  license:
    name: Proprietary
    identifier: LicenseRef-Proprietary
servers:
  - url: https://{appSlug}.opencloud.ai
    description: Deployed application origin
    variables:
      appSlug:
        default: example
        description: The app slug returned by the OpenCloud control plane.
tags:
  - name: Discovery
    description: Discover the active deployment and its pinned JavaScript SDK.
  - name: Session
    description: SDK-owned session brokering for the current app origin.
  - name: Database
    description: App-scoped PostgREST access governed by PostgreSQL RLS.
  - name: Storage
    description: App-scoped object storage governed by the manifest authorization model.
  - name: Functions
    description: App-defined Edge Functions.
  - name: Telemetry
    description: Safe app-bound runtime activity aggregates.
  - name: Realtime
    description: Private app-scoped WebSocket broadcast channels.
x-opencloud-realtime:
  url: wss://{appSlug}.opencloud.ai/realtime/v1/websocket
  description: |
    WebSocket endpoint used by `opencloud.realtime.channel(name)`. OpenAPI
    cannot describe the Phoenix channel protocol. The SDK owns the
    app-qualified topic, access token, heartbeat, reconnect, and broadcast
    envelopes; application code should not open this socket directly.
paths:
  /_opencloud/config:
    get:
      operationId: getRuntimeConfig
      tags: [Discovery]
      summary: Get runtime configuration
      description: |
        Returns the current app identity, deployment version, app Storage
        bucket, Function base path, and exact JavaScript SDK module pinned by
        the active deployment. Agents should call this before importing the
        SDK instead of guessing a versioned module URL.
      security: []
      responses:
        "200":
          description: Active app runtime configuration.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RuntimeConfig"
        "404":
          $ref: "#/components/responses/NotFound"
        "503":
          $ref: "#/components/responses/Unavailable"
  /_opencloud/session/v2:
    get:
      operationId: brokerRuntimeSession
      tags: [Session]
      summary: Broker the current browser session
      description: |
        Internal session-broker endpoint used by `opencloud.session()` and the
        SDK's authenticated request helpers. Application code should use the
        SDK instead of calling this route or handling `accessToken` directly.

        A signed-out visitor to a public app receives `200` with
        `{ "session": null }`. A private app without an admitted session
        receives `401`.
      security:
        - openCloudSession: []
        - {}
      responses:
        "200":
          description: Current session, or `null` for a signed-out public visitor.
          headers:
            Cache-Control:
              schema:
                type: string
                const: no-store
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SessionEnvelope"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
  /rest/v1/{resource}:
    parameters:
      - $ref: "#/components/parameters/Resource"
    get:
      operationId: selectRows
      tags: [Database]
      summary: Read rows from a table or view
      description: |
        Select rows through PostgREST. The deployed app's schema and PostgreSQL
        RLS policies determine the available resources, columns, and rows.

        PostgREST column filters are expressed as additional query parameters,
        for example `id=eq.<uuid>`. Prefer
        `opencloud.rest.request("items?select=id,title")`.
      security:
        - appApiKey: []
          appUser: []
        - appApiKey: []
          appAnonymousBearer: []
      parameters:
        - $ref: "#/components/parameters/Select"
        - $ref: "#/components/parameters/Order"
        - $ref: "#/components/parameters/Limit"
        - $ref: "#/components/parameters/Offset"
      responses:
        "200":
          description: Rows visible under the active RLS policies.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/DynamicRecord"
        "400":
          $ref: "#/components/responses/RuntimeError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
    post:
      operationId: insertRows
      tags: [Database]
      summary: Insert rows
      description: |
        Insert one object or an array of objects. Add
        `Prefer: return=representation` to receive inserted rows. RLS and
        database constraints remain authoritative.
      security:
        - appApiKey: []
          appUser: []
        - appApiKey: []
          appAnonymousBearer: []
      parameters:
        - $ref: "#/components/parameters/Prefer"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              oneOf:
                - $ref: "#/components/schemas/DynamicRecord"
                - type: array
                  minItems: 1
                  items:
                    $ref: "#/components/schemas/DynamicRecord"
      responses:
        "201":
          description: Row inserted. The response body depends on the `Prefer` header.
          content:
            application/json:
              schema:
                oneOf:
                  - type: array
                    items:
                      $ref: "#/components/schemas/DynamicRecord"
                  - type: "null"
        "400":
          $ref: "#/components/responses/RuntimeError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "409":
          $ref: "#/components/responses/Conflict"
    patch:
      operationId: updateRows
      tags: [Database]
      summary: Update selected rows
      description: |
        Update rows selected by PostgREST filter query parameters. Always send
        a narrow filter for mutations. RLS can further restrict the affected
        set.
      security:
        - appApiKey: []
          appUser: []
        - appApiKey: []
          appAnonymousBearer: []
      parameters:
        - $ref: "#/components/parameters/Prefer"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/DynamicRecord"
      responses:
        "200":
          description: Updated rows when representation is requested.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/DynamicRecord"
        "204":
          description: Rows updated without a response representation.
        "400":
          $ref: "#/components/responses/RuntimeError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
    delete:
      operationId: deleteRows
      tags: [Database]
      summary: Delete selected rows
      description: |
        Delete rows selected by PostgREST filter query parameters. Always send
        a narrow filter. RLS can further restrict the affected set.
      security:
        - appApiKey: []
          appUser: []
        - appApiKey: []
          appAnonymousBearer: []
      parameters:
        - $ref: "#/components/parameters/Prefer"
      responses:
        "200":
          description: Deleted rows when representation is requested.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/DynamicRecord"
        "204":
          description: Rows deleted without a response representation.
        "400":
          $ref: "#/components/responses/RuntimeError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
  /rest/v1/rpc/{databaseFunction}:
    post:
      operationId: callDatabaseFunction
      tags: [Database]
      summary: Call an exposed PostgreSQL function
      description: |
        Calls an app-defined PostgreSQL function exposed by PostgREST.
        Function permissions and RLS remain authoritative. Prefer ordinary
        table access unless an RPC is deliberately part of the app schema.
      security:
        - appApiKey: []
          appUser: []
        - appApiKey: []
          appAnonymousBearer: []
      parameters:
        - name: databaseFunction
          in: path
          required: true
          description: Exposed PostgreSQL function name.
          schema:
            type: string
            pattern: "^[a-z_][a-z0-9_]*$"
      requestBody:
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/DynamicRecord"
      responses:
        "200":
          description: App-defined function result.
          content:
            application/json:
              schema: {}
        "204":
          description: Function completed without a response body.
        "400":
          $ref: "#/components/responses/RuntimeError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
  /storage/v1/object/{bucket}/{objectPath}:
    parameters:
      - $ref: "#/components/parameters/Bucket"
      - $ref: "#/components/parameters/ObjectPath"
    get:
      operationId: downloadObject
      tags: [Storage]
      summary: Download an object
      description: |
        Download an object from the runtime-advertised app bucket. With
        `owner-prefix` authorization, the first decoded object-path segment
        must be the current user's UUID.
      security:
        - appApiKey: []
          appUser: []
        - appApiKey: []
          appAnonymousBearer: []
      responses:
        "200":
          description: Object bytes.
          content:
            "*/*":
              schema:
                type: string
                format: binary
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
    post:
      operationId: uploadObject
      tags: [Storage]
      summary: Upload an object
      description: |
        Upload bytes to a new object. Use the exact `storageBucket` returned
        by runtime configuration. Encode each object-name segment separately.
      security:
        - appApiKey: []
          appUser: []
      parameters:
        - name: x-upsert
          in: header
          required: false
          description: Set to `true` to replace an existing object.
          schema:
            type: string
            enum: ["true", "false"]
      requestBody:
        required: true
        content:
          "*/*":
            schema:
              type: string
              format: binary
      responses:
        "200":
          description: Object uploaded or replaced.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/StorageObjectResult"
        "400":
          $ref: "#/components/responses/RuntimeError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "409":
          $ref: "#/components/responses/Conflict"
    delete:
      operationId: deleteObject
      tags: [Storage]
      summary: Delete an object
      security:
        - appApiKey: []
          appUser: []
      responses:
        "200":
          description: Object deleted.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/StorageObjectResult"
        "204":
          description: Object deleted without a response body.
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
  /functions/v1/{functionName}:
    parameters:
      - $ref: "#/components/parameters/FunctionName"
    get:
      operationId: invokeFunctionWithGet
      tags: [Functions]
      summary: Invoke an app Function with GET
      description: Function request and response schemas are defined by the app.
      security:
        - appApiKey: []
          appUser: []
        - appApiKey: []
          appAnonymousBearer: []
      responses:
        "200":
          $ref: "#/components/responses/FunctionResult"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
    post:
      operationId: invokeFunctionWithPost
      tags: [Functions]
      summary: Invoke an app Function with POST
      description: |
        Use `functions.invoke()` for a Function declared with `verifyJwt: true`
        and `functions.invokePublic()` for one declared with `verifyJwt: false`.
      security:
        - appApiKey: []
          appUser: []
        - appApiKey: []
          appAnonymousBearer: []
      requestBody:
        content:
          application/json:
            schema: {}
          text/plain:
            schema:
              type: string
          application/octet-stream:
            schema:
              type: string
              format: binary
      responses:
        "200":
          $ref: "#/components/responses/FunctionResult"
        "201":
          $ref: "#/components/responses/FunctionResult"
        "202":
          $ref: "#/components/responses/FunctionResult"
        "204":
          description: Function completed without a response body.
        "400":
          $ref: "#/components/responses/RuntimeError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "500":
          $ref: "#/components/responses/RuntimeError"
    put:
      operationId: invokeFunctionWithPut
      tags: [Functions]
      summary: Invoke an app Function with PUT
      security:
        - appApiKey: []
          appUser: []
        - appApiKey: []
          appAnonymousBearer: []
      requestBody:
        content:
          application/json:
            schema: {}
          application/octet-stream:
            schema:
              type: string
              format: binary
      responses:
        "200":
          $ref: "#/components/responses/FunctionResult"
        "204":
          description: Function completed without a response body.
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
    patch:
      operationId: invokeFunctionWithPatch
      tags: [Functions]
      summary: Invoke an app Function with PATCH
      security:
        - appApiKey: []
          appUser: []
        - appApiKey: []
          appAnonymousBearer: []
      requestBody:
        content:
          application/json:
            schema: {}
      responses:
        "200":
          $ref: "#/components/responses/FunctionResult"
        "204":
          description: Function completed without a response body.
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
    delete:
      operationId: invokeFunctionWithDelete
      tags: [Functions]
      summary: Invoke an app Function with DELETE
      security:
        - appApiKey: []
          appUser: []
        - appApiKey: []
          appAnonymousBearer: []
      responses:
        "200":
          $ref: "#/components/responses/FunctionResult"
        "204":
          description: Function completed without a response body.
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
  /_opencloud/telemetry/summary:
    get:
      operationId: getRuntimeTelemetrySummary
      tags: [Telemetry]
      summary: Get safe runtime activity aggregates
      description: |
        Returns a host-bound 24-hour aggregate with no raw logs, request paths,
        internal labels, or datasource credentials. Missing activity is quiet
        or unknown, never proof of health.
      security:
        - openCloudSession: []
        - {}
      responses:
        "200":
          description: Current safe telemetry and latest usage rollup.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TelemetrySummary"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "502":
          $ref: "#/components/responses/Unavailable"
  /_opencloud/telemetry/metrics:
    post:
      operationId: ingestCustomMetrics
      tags: [Telemetry]
      summary: Write deployment-declared custom metrics
      description: |
        Accepts a batch of counters and gauges declared by the active
        deployment. The app is derived from the exact request host; callers
        cannot submit an app or deployment ID. Writes are limited to 20 per
        request, 1,200 per app per minute, and 100,000 retained points per app.
      security:
        - openCloudSession: []
        - {}
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CustomMetricWriteRequest"
      responses:
        "202":
          description: Measurements accepted or recognized as exact retries.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CustomMetricWriteResult"
        "400":
          $ref: "#/components/responses/RuntimeError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/RuntimeError"
        "429":
          $ref: "#/components/responses/RuntimeError"
components:
  securitySchemes:
    openCloudSession:
      type: apiKey
      in: cookie
      name: oc_session
      description: |
        HttpOnly OpenCloud browser session. The browser and SDK forward it;
        application JavaScript cannot read it.
    appApiKey:
      type: apiKey
      in: header
      name: apikey
      description: |
        Public project identity discovered through runtime configuration and
        supplied by `@opencloud/js`. It is not a secret.
    appUser:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: |
        Current user token brokered and refreshed by `@opencloud/js`. Do not
        extract, decode, persist, or manually supply this token in app code.
    appAnonymousBearer:
      type: http
      scheme: bearer
      description: |
        Anonymous project identity repeated as a bearer credential by the SDK
        for a deliberately public operation.
  parameters:
    Resource:
      name: resource
      in: path
      required: true
      description: App-defined table or view name.
      schema:
        type: string
        pattern: "^[a-z_][a-z0-9_]*$"
    Select:
      name: select
      in: query
      required: false
      description: Comma-separated PostgREST projection.
      schema:
        type: string
    Order:
      name: order
      in: query
      required: false
      description: PostgREST ordering expression, such as `created_at.desc`.
      schema:
        type: string
    Limit:
      name: limit
      in: query
      required: false
      schema:
        type: integer
        minimum: 0
    Offset:
      name: offset
      in: query
      required: false
      schema:
        type: integer
        minimum: 0
    Prefer:
      name: Prefer
      in: header
      required: false
      description: PostgREST preferences, such as `return=representation`.
      schema:
        type: string
    Bucket:
      name: bucket
      in: path
      required: true
      description: Use only `storageBucket` from `GET /_opencloud/config`.
      schema:
        type: string
        pattern: "^app-[0-9a-f-]{36}$"
    ObjectPath:
      name: objectPath
      in: path
      required: true
      description: |
        Slash-separated object name. Encode every segment independently. This
        parameter may span multiple URL path segments.
      allowReserved: true
      schema:
        type: string
        minLength: 1
    FunctionName:
      name: functionName
      in: path
      required: true
      description: Function name declared in `opencloud.yaml`.
      schema:
        type: string
        pattern: "^[a-z][a-z0-9-]{0,62}$"
  schemas:
    RuntimeConfig:
      type: object
      additionalProperties: false
      required:
        - appId
        - deploymentVersion
        - visibility
        - supabaseUrl
        - supabaseAnonKey
        - storageBucket
        - functionsBasePath
        - javascriptSdk
        - browserClient
        - environment
      properties:
        appId:
          type: string
          format: uuid
        deploymentVersion:
          type: [string, "null"]
        visibility:
          type: string
          enum: [public, private]
        supabaseUrl:
          type: string
          format: uri
          description: Same-origin runtime base URL.
        supabaseAnonKey:
          type: string
          description: Public project identity consumed by the SDK. It is not a secret.
        storageBucket:
          type: string
          pattern: "^app-[0-9a-f-]{36}$"
        functionsBasePath:
          type: string
          const: /functions/v1
        javascriptSdk:
          $ref: "#/components/schemas/JavaScriptSdkConfig"
        browserClient:
          type: string
          description: Compatibility alias for `javascriptSdk.module`.
        environment:
          type: string
          example: prod
    JavaScriptSdkConfig:
      type: object
      additionalProperties: false
      required: [package, version, module, types, docs]
      properties:
        package:
          type: string
          const: "@opencloud/js"
        version:
          type: string
          example: 0.2.2
        module:
          type: string
          example: /_opencloud/sdk/js/v0.2.2/index.js
        types:
          type: string
          example: /_opencloud/sdk/js/v0.2.2/index.d.ts
        docs:
          type: string
          format: uri
    SessionEnvelope:
      type: object
      additionalProperties: false
      required: [session]
      properties:
        session:
          oneOf:
            - $ref: "#/components/schemas/WireSession"
            - type: "null"
    WireSession:
      type: object
      additionalProperties: false
      description: |
        Internal SDK wire shape. `opencloud.session()` deliberately removes
        `accessToken` before returning the safe public session object.
      required:
        - appId
        - userId
        - profile
        - accessToken
        - accessTokenExpiresAt
        - refreshAfter
        - sessionExpiresAt
      properties:
        appId:
          type: string
          format: uuid
        userId:
          type: string
          format: uuid
        profile:
          $ref: "#/components/schemas/SessionProfile"
        accessToken:
          type: string
          writeOnly: true
          description: Internal bearer token consumed only by the SDK.
        accessTokenExpiresAt:
          type: string
          format: date-time
        refreshAfter:
          type: string
          format: date-time
        sessionExpiresAt:
          type: string
          format: date-time
    SessionProfile:
      type: object
      additionalProperties: false
      required: [email, displayName, avatarUrl]
      properties:
        email:
          type: [string, "null"]
          format: email
        displayName:
          type: [string, "null"]
        avatarUrl:
          type: [string, "null"]
          format: uri
    DynamicRecord:
      type: object
      description: App-defined database record.
      additionalProperties: true
    StorageObjectResult:
      type: object
      description: Storage service result. Fields can vary by operation.
      additionalProperties: true
    CustomMetricWriteRequest:
      type: object
      additionalProperties: false
      required: [measurements]
      properties:
        measurements:
          type: array
          minItems: 1
          maxItems: 20
          items:
            $ref: "#/components/schemas/CustomMetricMeasurement"
    CustomMetricMeasurement:
      type: object
      additionalProperties: false
      required: [name, value]
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 63
          pattern: "^[a-z][a-z0-9_]*$"
        value:
          type: number
          minimum: -1000000000000000
          maximum: 1000000000000000
        dimensions:
          type: object
          maxProperties: 3
          propertyNames:
            pattern: "^[a-z][a-z0-9_]{0,39}$"
          additionalProperties:
            type: string
            minLength: 1
            maxLength: 40
          default: {}
        idempotencyKey:
          type: string
          minLength: 8
          maxLength: 120
          pattern: "^[A-Za-z0-9._:-]+$"
    CustomMetricWriteResult:
      type: object
      additionalProperties: false
      required: [accepted, duplicates, recordedAt]
      properties:
        accepted:
          type: integer
          minimum: 0
          maximum: 20
        duplicates:
          type: integer
          minimum: 0
          maximum: 20
        recordedAt:
          type: string
          format: date-time
    TelemetrySummary:
      type: object
      additionalProperties: false
      required: [appId, asOf, usage, activity]
      properties:
        appId:
          type: string
          format: uuid
        asOf:
          type: string
          format: date-time
        usage:
          oneOf:
            - $ref: "#/components/schemas/UsageRollup"
            - type: "null"
        activity:
          $ref: "#/components/schemas/RuntimeActivity"
    UsageRollup:
      type: object
      additionalProperties: false
      required:
        - windowStart
        - windowEnd
        - calculationVersion
        - completeness
        - metrics
        - createdAt
      properties:
        windowStart:
          type: string
          format: date-time
        windowEnd:
          type: string
          format: date-time
        calculationVersion:
          type: string
        completeness:
          type: string
          enum: [complete, partial, corrected]
        metrics:
          type: object
          additionalProperties: true
        createdAt:
          type: string
          format: date-time
    RuntimeActivity:
      type: object
      additionalProperties: false
      required: [window, telemetry, surfaces]
      properties:
        window:
          type: object
          additionalProperties: false
          required: [from, to, seconds]
          properties:
            from:
              type: string
              format: date-time
            to:
              type: string
              format: date-time
            seconds:
              type: integer
              minimum: 0
        telemetry:
          type: object
          additionalProperties: false
          required:
            - status
            - latestIngestedAt
            - ingestionLagSeconds
            - sampledEntries
            - truncated
          properties:
            status:
              type: string
              enum: [available, unavailable]
            latestIngestedAt:
              type: [string, "null"]
              format: date-time
            ingestionLagSeconds:
              type: [number, "null"]
              minimum: 0
            sampledEntries:
              type: integer
              minimum: 0
            truncated:
              type: boolean
        surfaces:
          type: object
          additionalProperties: false
          required: [page, rest, storage, realtime, function, cron]
          properties:
            page:
              $ref: "#/components/schemas/SurfaceActivity"
            rest:
              $ref: "#/components/schemas/SurfaceActivity"
            storage:
              $ref: "#/components/schemas/SurfaceActivity"
            realtime:
              $ref: "#/components/schemas/SurfaceActivity"
            function:
              $ref: "#/components/schemas/SurfaceActivity"
            cron:
              $ref: "#/components/schemas/SurfaceActivity"
    SurfaceActivity:
      type: object
      additionalProperties: false
      required: [lastActivityAt, requests24h, errors24h, lastStatus]
      properties:
        lastActivityAt:
          type: [string, "null"]
          format: date-time
        requests24h:
          type: integer
          minimum: 0
        errors24h:
          type: integer
          minimum: 0
        lastStatus:
          type: [integer, "null"]
          minimum: 100
          maximum: 599
    Error:
      type: object
      description: Runtime services can add service-specific fields.
      properties:
        statusCode:
          type: integer
        code:
          type: string
        message:
          type: string
        details: {}
        requestId:
          type: string
      additionalProperties: true
  responses:
    FunctionResult:
      description: App-defined Function response.
      content:
        application/json:
          schema: {}
        text/plain:
          schema:
            type: string
        application/octet-stream:
          schema:
            type: string
            format: binary
    RuntimeError:
      description: Invalid request or runtime service error.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    Unauthorized:
      description: No valid app session or bearer identity.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    Forbidden:
      description: The current identity is not permitted by app access rules or RLS.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    NotFound:
      description: App, resource, Function, row, or object not found.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    Conflict:
      description: Resource already exists or violates a uniqueness constraint.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    Unavailable:
      description: App runtime is not currently available.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
