{
  "openapi": "3.1.0",
  "info": {
    "title": "Host An App API",
    "version": "1.0.0",
    "description": "Zero-config ephemeral hosting for HTML, Markdown, Mermaid, and JSON/CSV data content. Deploy via REST or MCP, get a public URL instantly. Deployments expire after 7 days. Stateful apps are built as projects \u2014 over MCP (manage_project, manage_backend, manage_frontend) or over REST (/api/v1/projects/{id}\u2026, full parity); visitors invoke author-defined actions (POST /{slug}/api/op), raw SQL/schema stay owner-only (Bearer namespace credential).\n\nLimits \u2014 rate limits are per client IP: deploys and MCP 30 req/min (burst 15); project runtime API (/op public, /query+/schema owner-only) 120 req/min per IP plus a 300 req/min per-deployment bucket. Exceeding a rate limit returns 429 with a Retry-After: 60 header. Request bodies are capped at 1 MB (64 KB for op/query bodies) and return 413 above that; a project backend database is hard-capped at 5 MB. Capacity caps (5000 active deployments, 250 backend databases, 100 MB free disk floor) reject deploys with 503.",
    "contact": {
      "url": "https://hostan.app",
      "name": "Host An App"
    },
    "license": {
      "name": "MIT",
      "url": "https://github.com/VictorBusque/hostanapp/blob/main/LICENSE"
    }
  },
  "servers": [
    {
      "url": "https://api.hostan.app",
      "description": "Production (api subdomain)"
    },
    {
      "url": "https://sites.hostan.app",
      "description": "Production (sites subdomain \u2014 same API, same-origin for auth/visit)"
    },
    {
      "url": "https://hostan.app",
      "description": "Production (landing and documentation site)"
    }
  ],
  "tags": [
    {
      "name": "deploy",
      "description": "Anonymous quick-share deployments"
    },
    {
      "name": "projects",
      "description": "Namespace-owned project envelopes (namespace credential)"
    },
    {
      "name": "database",
      "description": "Project runtime: public actions (POST /{slug}/api/op) plus owner-only SQL/schema (Bearer namespace credential)"
    }
  ],
  "paths": {
    "/api/v1/deploy": {
      "post": {
        "tags": [
          "deploy"
        ],
        "summary": "Deploy content and get a public URL",
        "description": "Anonymous quick-share only: content fields (`content`/`type`/`name`/`title`/`one_time`/`password`) bound to the seeded free project with the default TTL. Durable, manageable pages live under namespace \u2192 project \u2192 frontend (manage_project/manage_frontend/manage_backend over MCP, or /api/v1/projects over REST with a namespace credential).",
        "operationId": "deploy",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DeployRequest"
              },
              "examples": {
                "html": {
                  "summary": "HTML app",
                  "value": {
                    "name": "my-app",
                    "content": "<!DOCTYPE html><html><body>Hello!</body></html>",
                    "type": "html"
                  }
                },
                "markdown": {
                  "summary": "Markdown document",
                  "value": {
                    "name": "my-doc",
                    "content": "# Hello\n\nThis is **Markdown**.",
                    "type": "markdown",
                    "title": "My Doc"
                  }
                },
                "mermaid": {
                  "summary": "Mermaid diagram",
                  "value": {
                    "name": "my-diagram",
                    "content": "graph TD\n  A[Start] --> B[End]",
                    "type": "mermaid",
                    "title": "My Diagram"
                  }
                },
                "json": {
                  "summary": "JSON dataset viewer",
                  "value": {
                    "name": "api-response",
                    "content": "{\"users\": [{\"name\": \"ada\", \"role\": \"admin\"}]}",
                    "type": "json",
                    "title": "API Response"
                  }
                },
                "csv": {
                  "summary": "CSV data explorer",
                  "value": {
                    "name": "q3-sales",
                    "content": "month,revenue\n2026-07,42310\n2026-08,38990\n2026-09,47125",
                    "type": "csv",
                    "title": "Q3 Sales"
                  }
                },
                "password": {
                  "summary": "Password-protected",
                  "value": {
                    "name": "secret",
                    "content": "<h1>Secret</h1>",
                    "password": "s3cret"
                  }
                },
                "oneTime": {
                  "summary": "One-time (burn after read)",
                  "value": {
                    "name": "burn-after-reading",
                    "content": "<html><body>Secret</body></html>",
                    "one_time": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Deployment created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DeployResponse"
                },
                "example": {
                  "id": "dep_01k8x7n1f8k0",
                  "slug": "my-app",
                  "url": "https://sites.hostan.app/my-app",
                  "created_at": "2026-06-10T20:00:00Z",
                  "expires_at": "2026-06-17T20:00:00Z"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "503": {
            "$ref": "#/components/responses/Capacity"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/{slug}/api/op": {
      "post": {
        "tags": [
          "database"
        ],
        "summary": "Invoke one author-defined action on a project frontend's backend",
        "description": "Public visitor surface: the slug resolves server-side to the frontend's project backend, and the named action runs its single gated statement (SELECT/INSERT/UPDATE/DELETE, WITH allowed, :name binds only) from the backend_actions ledger. Params must match the declared names exactly (missing/extra keys \u2192 400 naming the diff). DML with top-level RETURNING returns rows. Pages call this via hostan.db.op(name, params); raw SQL never reaches the browser.",
        "operationId": "databaseOp",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9]([a-z0-9\\-]*[a-z0-9])?$"
            },
            "description": "Project frontend slug"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DBOpRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Action result (same shape as a query result)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DBQueryResponse"
                }
              }
            }
          },
          "400": {
            "description": "Missing action name, unknown param contract, or invalid JSON body"
          },
          "404": {
            "description": "Unknown action, or slug is not a project frontend with a live backend"
          },
          "409": {
            "description": "Constraint violation"
          },
          "413": {
            "description": "Database size cap reached (writes)"
          },
          "429": {
            "description": "Rate limit exceeded"
          },
          "503": {
            "description": "Database busy"
          }
        }
      }
    },
    "/{slug}/api/query": {
      "post": {
        "tags": [
          "database"
        ],
        "summary": "Run one SQL statement against a project frontend's backend (owner-only)",
        "description": "Owner-only inspection path: requires the slug's owning namespace credential as Authorization: Bearer hstn_\u2026 (401 otherwise, pointing at /op; the credential must never appear in a page). Allowed verbs: SELECT, INSERT, UPDATE, DELETE (WITH prefix allowed). DDL, PRAGMA, ATTACH, transactions, EXPLAIN and multi-statement input are rejected with 400. Rows capped at 500 (truncated flag). 5 MB backend cap.",
        "operationId": "databaseQuery",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9]([a-z0-9\\-]*[a-z0-9])?$"
            },
            "description": "Project frontend slug"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DBQueryRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Query result",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DBQueryResponse"
                }
              }
            }
          },
          "400": {
            "description": "Invalid SQL, disallowed statement, or multi-statement input"
          },
          "401": {
            "description": "Missing/invalid namespace credential (owner-only; visitors use POST /{slug}/api/op)"
          },
          "404": {
            "description": "Slug is not a project frontend with a live backend"
          },
          "409": {
            "description": "Constraint violation"
          },
          "413": {
            "description": "Database size cap reached (writes)"
          },
          "429": {
            "description": "Rate limit exceeded"
          },
          "503": {
            "description": "Database busy"
          }
        }
      }
    },
    "/{slug}/api/schema": {
      "get": {
        "tags": [
          "database"
        ],
        "summary": "Get a project frontend's backend schema (owner-only)",
        "description": "Returns the live-introspected tables and columns. Owner-only: requires the slug's owning namespace credential as Authorization: Bearer hstn_\u2026 (401 otherwise); the credential must never appear in a page. Only project frontends answer; slugs without a live backend answer 404.",
        "operationId": "databaseSchema",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9]([a-z0-9\\-]*[a-z0-9])?$"
            },
            "description": "Project frontend slug"
          }
        ],
        "responses": {
          "200": {
            "description": "Schema",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DBSchemaResponse"
                }
              }
            }
          },
          "404": {
            "description": "Slug is not a project frontend with a live backend"
          },
          "401": {
            "description": "Missing/invalid namespace credential (owner-only; visitors use POST /{slug}/api/op)"
          },
          "429": {
            "description": "Rate limit exceeded"
          }
        }
      }
    },
    "/api/v1/projects": {
      "post": {
        "tags": [
          "projects"
        ],
        "summary": "Create a project owned by the credential's namespace",
        "operationId": "createProject",
        "description": "Creates a project envelope: identity and an expiry policy (`ttl_seconds` or `permanent`, freely chosen; omitted defaults to permanent). The namespace credential in the bearer position owns the project and manages it \u2014 with its frontends and backend \u2014 by project id on every later call. No per-project token is minted or returned.",
        "security": [
          {
            "NamespaceCredential": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ProjectCreateRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProjectCreateResponse"
                }
              }
            }
          },
          "400": {
            "description": "Invalid input (missing name, contradictory expiry inputs).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          }
        }
      },
      "get": {
        "tags": [
          "projects"
        ],
        "summary": "List the namespace's projects",
        "operationId": "listProjects",
        "description": "Lists every project owned by the namespace presenting the bearer credential.",
        "security": [
          {
            "NamespaceCredential": []
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProjectListResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/api/v1/projects/{id}": {
      "get": {
        "tags": [
          "projects"
        ],
        "summary": "Get a project's detail",
        "operationId": "getProject",
        "description": "Returns the project's identity plus the durable visit rollup over its members and the member inventory (slug, type, state). The last few seconds of visits may be missing (the hot path buffers visits for one flush window) \u2014 bounded loss, never a double count.",
        "security": [
          {
            "NamespaceCredential": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The project id (UUIDv7) returned by project creation; the project must be live and owned by the credential's namespace.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The project detail.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProjectDetail"
                }
              }
            }
          },
          "401": {
            "description": "Missing, invalid, or revoked credential."
          },
          "403": {
            "description": "The credential does not own this deployment."
          },
          "404": {
            "description": "Deployment not found."
          }
        }
      },
      "patch": {
        "tags": [
          "projects"
        ],
        "summary": "Update a project's identity",
        "operationId": "updateProject",
        "description": "Identity-only update: name and/or description. Omitted (or null) fields are left unchanged; a present empty string clears the description; an empty name is rejected (projects are never nameless \u2014 renaming is how a name changes). Expiry, namespace, and structure never change here.",
        "security": [
          {
            "NamespaceCredential": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The project id (UUIDv7) returned by project creation; the project must be live and owned by the credential's namespace.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "description": "Fields to change.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ProjectUpdateRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The updated project.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProjectView"
                }
              }
            }
          },
          "400": {
            "description": "Invalid input (empty name, nothing to update).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "Missing, invalid, or revoked credential."
          },
          "403": {
            "description": "The credential does not own this deployment."
          },
          "404": {
            "description": "Deployment not found."
          }
        }
      },
      "delete": {
        "tags": [
          "projects"
        ],
        "summary": "Delete a project (teardown cascade)",
        "operationId": "deleteProject",
        "description": "Runs the teardown cascade: every member frontend is removed, then the backend tier, then the project row \u2014 freeing every slug. Retrying after success (or against an already-deleted project) is a clean 204. The well-known free project is refused.",
        "security": [
          {
            "NamespaceCredential": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The project id (UUIDv7) returned by project creation; the project must be live and owned by the credential's namespace.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Deleted (or already gone)."
          },
          "401": {
            "description": "Missing, invalid, or revoked credential."
          },
          "403": {
            "description": "The credential does not own this deployment."
          },
          "404": {
            "description": "Deployment not found."
          }
        }
      }
    },
    "/api/v1/projects/{id}/frontends": {
      "post": {
        "tags": [
          "project frontends"
        ],
        "summary": "Publish a frontend owned by the project",
        "operationId": "createProjectFrontend",
        "description": "Deploys a static frontend (html, markdown, mermaid, json, csv \u2014 the same rendering pipeline as quick shares) owned by the project. The page inherits the project's clock (no per-frontend TTL, no password, no one_time), keeps a global flat slug (on collision a suffixed variant is chosen), and \u2014 once the project has a backend \u2014 receives the tokenless hostan.db runtime bound to its own slug.",
        "security": [
          {
            "NamespaceCredential": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The project id (UUIDv7) returned by project creation; the project must be live and owned by the credential's namespace.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "description": "Content and optional slug/title.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/FrontendCreateRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Deployed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FrontendDeployResponse"
                }
              }
            }
          },
          "400": {
            "description": "Invalid input (content required; database/app types and password/one_time are rejected).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "Missing, invalid, or revoked credential."
          },
          "403": {
            "description": "The credential does not own this deployment."
          },
          "404": {
            "description": "Deployment not found."
          },
          "413": {
            "description": "Body > 1 MB (nginx client_max_body_size + app MaxBytesReader)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded (429 + Retry-After: 60)",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "Service at capacity (5000 active or <100 MB free) \u2014 503 + Retry-After: 60 with reason",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "string",
                  "example": "60"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      },
      "get": {
        "tags": [
          "project frontends"
        ],
        "summary": "List the project's frontends",
        "operationId": "listProjectFrontends",
        "description": "Lists every live member frontend (creating, active, deleting \u2014 an interrupted teardown stays visible), oldest first, metadata only.",
        "security": [
          {
            "NamespaceCredential": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The project id (UUIDv7) returned by project creation; the project must be live and owned by the credential's namespace.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The member inventory.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FrontendListResponse"
                }
              }
            }
          },
          "401": {
            "description": "Missing, invalid, or revoked credential."
          },
          "403": {
            "description": "The credential does not own this deployment."
          },
          "404": {
            "description": "Deployment not found."
          }
        }
      }
    },
    "/api/v1/projects/{id}/frontends/{target}": {
      "get": {
        "tags": [
          "project frontends"
        ],
        "summary": "Get a frontend's metadata",
        "operationId": "getProjectFrontend",
        "description": "One member frontend by slug or deployment id. Only active members resolve; a member already marked deleting answers 404 like a missing one.",
        "security": [
          {
            "NamespaceCredential": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The project id (UUIDv7) returned by project creation; the project must be live and owned by the credential's namespace.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "target",
            "in": "path",
            "required": true,
            "description": "The frontend's slug or its immutable deployment id (a UUID shape dispatches as an id).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The member's metadata.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FrontendMetadata"
                }
              }
            }
          },
          "401": {
            "description": "Missing, invalid, or revoked credential."
          },
          "403": {
            "description": "The credential does not own this deployment."
          },
          "404": {
            "description": "Deployment not found."
          }
        }
      },
      "put": {
        "tags": [
          "project frontends"
        ],
        "summary": "Republish a frontend (new revision)",
        "operationId": "updateProjectFrontend",
        "description": "Publishes a new revision via an atomic swap. Content is required; the content type is fixed at create (an explicit mismatching type is rejected); slug, id, and expiry are immutable \u2014 a non-empty requested_slug answers 400.",
        "security": [
          {
            "NamespaceCredential": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The project id (UUIDv7) returned by project creation; the project must be live and owned by the credential's namespace.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "target",
            "in": "path",
            "required": true,
            "description": "The frontend's slug or its immutable deployment id (a UUID shape dispatches as an id).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "description": "The new content.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/FrontendUpdateRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Republished.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FrontendDeployResponse"
                }
              }
            }
          },
          "400": {
            "description": "Invalid input (content required, type mismatch).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "Missing, invalid, or revoked credential."
          },
          "403": {
            "description": "The credential does not own this deployment."
          },
          "404": {
            "description": "Deployment not found."
          },
          "413": {
            "description": "Body > 1 MB (nginx client_max_body_size + app MaxBytesReader)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded (429 + Retry-After: 60)",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "Service at capacity (5000 active or <100 MB free) \u2014 503 + Retry-After: 60 with reason",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "string",
                  "example": "60"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "project frontends"
        ],
        "summary": "Delete a frontend",
        "operationId": "deleteProjectFrontend",
        "description": "Idempotent removal: the slug is freed; its visit history is retired with its deployment id. Deleting an unknown frontend is a clean 204.",
        "security": [
          {
            "NamespaceCredential": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The project id (UUIDv7) returned by project creation; the project must be live and owned by the credential's namespace.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "target",
            "in": "path",
            "required": true,
            "description": "The frontend's slug or its immutable deployment id (a UUID shape dispatches as an id).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Deleted (or already gone)."
          },
          "401": {
            "description": "Missing, invalid, or revoked credential."
          },
          "403": {
            "description": "The credential does not own this deployment."
          }
        }
      }
    },
    "/api/v1/projects/{id}/backend": {
      "post": {
        "tags": [
          "project backend"
        ],
        "summary": "Provision the project's backend tier",
        "operationId": "createProjectBackend",
        "description": "Provisions the project's single server-side tier \u2014 one SQLite database, name \"default\" \u2014 from a DDL script: CREATE TABLE / CREATE INDEX / CREATE VIEW plus optional seed INSERTs (additive provisioning gate; at least one CREATE TABLE). Exactly one backend per project: a second create answers 409. Frontends published from then on embed the tokenless hostan.db runtime; earlier pages need one republish to receive it. Counts against the platform's database cap and inherits the project's clock.",
        "security": [
          {
            "NamespaceCredential": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The project id (UUIDv7) returned by project creation; the project must be live and owned by the credential's namespace.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "description": "The provisioning DDL.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BackendCreateRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Provisioned.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BackendView"
                }
              }
            }
          },
          "400": {
            "description": "Invalid input (empty or non-additive DDL).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "Missing, invalid, or revoked credential."
          },
          "403": {
            "description": "The credential does not own this deployment."
          },
          "404": {
            "description": "Deployment not found."
          },
          "409": {
            "description": "A deterministic create with an existing slug."
          },
          "429": {
            "description": "Rate limit exceeded (429 + Retry-After: 60)",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "Service at capacity (5000 active or <100 MB free) \u2014 503 + Retry-After: 60 with reason",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "string",
                  "example": "60"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      },
      "get": {
        "tags": [
          "project backend"
        ],
        "summary": "Get the backend's detail",
        "operationId": "getProjectBackend",
        "description": "Returns the spec (version, database, engine, max bytes), the schema derived live from the file (never from stored state), the migration ledger (the schema's only stored truth), the actions with their strict invocation contracts, the inventory (tables, bytes on disk), and any drift warnings. A project with no backend (the first-class static-only state) answers 404.",
        "security": [
          {
            "NamespaceCredential": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The project id (UUIDv7) returned by project creation; the project must be live and owned by the credential's namespace.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The tier detail.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BackendDetail"
                }
              }
            }
          },
          "401": {
            "description": "Missing, invalid, or revoked credential."
          },
          "403": {
            "description": "The credential does not own this deployment."
          },
          "404": {
            "description": "Deployment not found."
          }
        }
      },
      "patch": {
        "tags": [
          "project backend"
        ],
        "summary": "Apply one additive migration",
        "operationId": "migrateProjectBackend",
        "description": "Applies ONE additive migration per call: CREATE TABLE/INDEX/VIEW, ALTER TABLE \u2026 ADD COLUMN, seed INSERTs. DROP, DELETE, UPDATE, other ALTER forms, PRAGMA, ATTACH, VACUUM, EXPLAIN, and explicit transactions are rejected. The DDL commits in one transaction, then the ledger row is appended. An idempotent retry answers already_applied; the documented crash window (DDL live, ledger row missing) heals on re-send and answers healed.",
        "security": [
          {
            "NamespaceCredential": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The project id (UUIDv7) returned by project creation; the project must be live and owned by the credential's namespace.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "description": "The migration DDL and an optional note.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BackendMigrationRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Applied (or already applied / healed).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MigrationResult"
                }
              }
            }
          },
          "400": {
            "description": "Invalid input (gate rejection).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "Missing, invalid, or revoked credential."
          },
          "403": {
            "description": "The credential does not own this deployment."
          },
          "404": {
            "description": "Deployment not found."
          },
          "409": {
            "description": "Conflict (migration applied but not recorded \u2014 re-send the same DDL to heal).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded (429 + Retry-After: 60)",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "Service at capacity (5000 active or <100 MB free) \u2014 503 + Retry-After: 60 with reason",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "string",
                  "example": "60"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "project backend"
        ],
        "summary": "Tear down the backend tier",
        "operationId": "deleteProjectBackend",
        "description": "Idempotent tier teardown: cached handles closed, runtime directories removed, rows cascaded. Frontends keep serving; their action calls answer 404 until a new backend is provisioned and the pages are republished. Deleting when none exists is a clean 204.",
        "security": [
          {
            "NamespaceCredential": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The project id (UUIDv7) returned by project creation; the project must be live and owned by the credential's namespace.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Deleted (or already gone)."
          },
          "401": {
            "description": "Missing, invalid, or revoked credential."
          },
          "403": {
            "description": "The credential does not own this deployment."
          }
        }
      }
    },
    "/api/v1/projects/{id}/backend/actions": {
      "put": {
        "tags": [
          "project backend"
        ],
        "summary": "Define the public action surface",
        "operationId": "updateProjectBackendActions",
        "description": "Defines (or redefines) the batch of named statements pages may invoke tokenlessly via hostan.db.op(name, params) \u2014 the ONLY things a project page may run against the database. One SELECT/INSERT/UPDATE/DELETE per action (WITH allowed), :name binds only; an op call must provide exactly the declared params. Redefinition (same name, new sql) appends a new ledger version; deletion is not a thing \u2014 drop the action from the batch and stop calling it. Raw SQL over HTTP stays owner-only.",
        "security": [
          {
            "NamespaceCredential": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The project id (UUIDv7) returned by project creation; the project must be live and owned by the credential's namespace.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "description": "The action batch.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BackendActionsRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The current action definitions.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BackendActionsResponse"
                }
              }
            }
          },
          "400": {
            "description": "Invalid input (empty batch, bad name, ungated SQL).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "Missing, invalid, or revoked credential."
          },
          "403": {
            "description": "The credential does not own this deployment."
          },
          "404": {
            "description": "Deployment not found."
          },
          "429": {
            "description": "Rate limit exceeded (429 + Retry-After: 60)",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "503": {
            "description": "Service at capacity (5000 active or <100 MB free) \u2014 503 + Retry-After: 60 with reason",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "string",
                  "example": "60"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/{slug}/ws": {
      "get": {
        "summary": "Realtime room relay (WebSocket upgrade)",
        "description": "Upgrades to a WebSocket connection on a project frontend whose backend has `realtime: true` (#150). The slug resolves server-side to the frontend's project and its backend \u2014 no credential, no client-selectable backend; the endpoint is exactly as public as the page that serves it.\n\nProtocol: JSON text envelopes `{v: 1, type, data, client_message_id?}` client \u2192 server; the server stamps trusted `sender`/`room`/`ts` and fans out to every other member of the same backend + room. Platform events (`hostan:presence`, `hostan:join`, `hostan:leave`) cannot be forged. Ephemeral relay: no history, no replay, no cross-backend rooms.\n\nLimits: 16 KiB frames, ~10 msg/s per socket, 100 sockets per backend, 20 per client IP, 100 rooms per backend, room ids 1\u201364 chars `[A-Za-z0-9._~-]`. Same-host `Origin` is enforced for browser requests. Pages should use the injected `hostan.ws` runtime (`hostan.ws.connect(roomId)`) rather than dialing raw URLs.",
        "operationId": "realtimeConnect",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The project frontend's slug."
          },
          {
            "name": "room",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 64,
              "pattern": "^[A-Za-z0-9][A-Za-z0-9._~-]*$"
            },
            "description": "The room to join (scoped to this backend)."
          }
        ],
        "responses": {
          "101": {
            "description": "WebSocket upgrade accepted; the connection is now a realtime relay socket."
          },
          "400": {
            "description": "Invalid room id.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "Deployment disabled (safety status) or cross-origin upgrade refused."
          },
          "404": {
            "description": "Unknown slug, expired deployment, or the backend did not opt into realtime."
          },
          "429": {
            "description": "Connection quota reached (per backend, per IP, or global); includes `Retry-After`."
          }
        }
      }
    },
    "/api/v1/projects/{id}/payments/catalog": {
      "get": {
        "tags": [
          "project payments"
        ],
        "summary": "List the payments catalog",
        "operationId": "listPaymentsCatalog",
        "description": "Returns the project's payments catalog: products and prices. Prices are the purchasable units; the price id is the only identifier a visitor page ever handles (hostan.payments.checkout takes a price id, never an amount).",
        "security": [
          {
            "NamespaceCredential": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The project id; the project must be live and namespace-owned. A backend is only required for the hostan_payments entitlement mirror, not for the catalog.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The catalog.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PaymentsCatalog"
                }
              }
            }
          },
          "401": {
            "description": "Missing, invalid, or revoked credential."
          },
          "403": {
            "description": "The project belongs to another namespace or is the free project."
          },
          "404": {
            "description": "Project (or price) not found."
          },
          "429": {
            "description": "Rate limit exceeded (429 + Retry-After: 60)"
          }
        }
      },
      "post": {
        "tags": [
          "project payments"
        ],
        "summary": "Define a price (product created implicitly)",
        "operationId": "addPaymentsPrice",
        "description": "Creates one product (implicitly, from product_name/description) and one active price under it. amount is a positive integer in minor units (cents for usd); currency is ISO 4217, lowercased; recurring bills as a monthly subscription (Stripe mode=subscription, interval month); omitted or false is a one-time payment. Either way the entitlement is granted on the first verified success webhook. The returned price id is what pages pass to hostan.payments.checkout.",
        "security": [
          {
            "NamespaceCredential": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The project id.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PaymentsAddPriceRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Price created (active).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PaymentsPrice"
                }
              }
            }
          },
          "400": {
            "description": "Invalid input (non-positive amount, bad currency, missing label).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "description": "Missing, invalid, or revoked credential."
          },
          "403": {
            "description": "The project belongs to another namespace or is the free project."
          },
          "404": {
            "description": "Project (or price) not found."
          },
          "429": {
            "description": "Rate limit exceeded (429 + Retry-After: 60)"
          }
        }
      }
    },
    "/api/v1/projects/{id}/payments/catalog/{pid}": {
      "delete": {
        "tags": [
          "project payments"
        ],
        "summary": "Deactivate a price",
        "operationId": "deactivatePaymentsPrice",
        "description": "Marks a price unpurchasable. Past sessions keep their snapshots; the price row remains listed (inactive).",
        "security": [
          {
            "NamespaceCredential": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The project id.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "pid",
            "in": "path",
            "required": true,
            "description": "The price id to deactivate.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Deactivated.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PaymentsDeactivateResult"
                }
              }
            }
          },
          "401": {
            "description": "Missing, invalid, or revoked credential."
          },
          "403": {
            "description": "The project belongs to another namespace or is the free project."
          },
          "404": {
            "description": "Project (or price) not found."
          },
          "429": {
            "description": "Rate limit exceeded (429 + Retry-After: 60)"
          }
        }
      }
    },
    "/api/v1/projects/{id}/payments/sessions": {
      "get": {
        "tags": [
          "project payments"
        ],
        "summary": "List recent checkout sessions",
        "operationId": "listPaymentsSessions",
        "description": "Returns the project's recent checkout sessions, newest first, with their verdicts (open, paid, canceled, failed, refunded). Support view: verdicts change only through verified, deduplicated provider webhooks \u2014 never through redirects.",
        "security": [
          {
            "NamespaceCredential": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The project id.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum entries (default 100).",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Sessions.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PaymentsSessions"
                }
              }
            }
          },
          "401": {
            "description": "Missing, invalid, or revoked credential."
          },
          "403": {
            "description": "The project belongs to another namespace or is the free project."
          },
          "404": {
            "description": "Project (or price) not found."
          },
          "429": {
            "description": "Rate limit exceeded (429 + Retry-After: 60)"
          }
        }
      }
    },
    "/api/v1/projects/{id}/frontends/{target}/content": {
      "get": {
        "tags": [
          "project frontends"
        ],
        "summary": "Read a frontend's active source (conditional)",
        "operationId": "readFrontendContent",
        "description": "Returns the exact stored source of the frontend's active revision (never the rendered artifact). ?revision_id= reads a specific revision; ?known_content_sha256= returns {unchanged:true} with no body when the caller's held hash still matches.",
        "security": [
          {
            "NamespaceCredential": []
          }
        ],
        "parameters": [
          {
            "in": "query",
            "name": "revision_id",
            "schema": {
              "type": "string"
            },
            "description": "Read this revision's source instead of the active one."
          },
          {
            "in": "query",
            "name": "known_content_sha256",
            "schema": {
              "type": "string"
            },
            "description": "Conditional read: short-circuit to unchanged when it matches."
          }
        ],
        "responses": {
          "200": {
            "description": "ok"
          },
          "401": {
            "description": "Missing or invalid namespace credential."
          },
          "404": {
            "description": "Project or frontend not found."
          }
        }
      }
    },
    "/api/v1/projects/{id}/frontends/{target}/revisions": {
      "get": {
        "tags": [
          "project frontends"
        ],
        "summary": "List a frontend's revision metadata",
        "operationId": "listFrontendRevisions",
        "description": "Returns revision metadata (never source bodies), newest first, plus the current active revision id.",
        "security": [
          {
            "NamespaceCredential": []
          }
        ],
        "responses": {
          "200": {
            "description": "ok"
          },
          "401": {
            "description": "Missing or invalid namespace credential."
          },
          "404": {
            "description": "Project or frontend not found."
          }
        }
      },
      "post": {
        "tags": [
          "project frontends"
        ],
        "summary": "Create an immutable draft revision",
        "operationId": "createFrontendRevision",
        "description": "Renders, validates, and publishes an immutable draft revision under an unlisted _preview URL; the canonical page is untouched. base_revision_id must equal the current active revision. Same-content writes deduplicate to the original revision.",
        "security": [
          {
            "NamespaceCredential": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "base_revision_id": {
                    "type": "string",
                    "description": "The current active revision to derive from."
                  },
                  "content": {
                    "type": "string",
                    "description": "The new source (HTML, Markdown, Mermaid, JSON, or CSV)."
                  },
                  "title": {
                    "type": "string",
                    "description": "Optional page title (defaults to the active revision's)."
                  },
                  "message": {
                    "type": "string",
                    "description": "Optional audit note."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "ok"
          },
          "401": {
            "description": "Missing or invalid namespace credential."
          },
          "404": {
            "description": "Project or frontend not found."
          },
          "409": {
            "description": "Optimistic-concurrency conflict (revision_conflict): the release/draft base is stale."
          }
        }
      }
    },
    "/api/v1/projects/{id}/frontends/{target}/revisions/{rid}": {
      "get": {
        "tags": [
          "project frontends"
        ],
        "summary": "Get one revision's metadata",
        "operationId": "getFrontendRevision",
        "description": "Returns one revision's metadata (identity, number, state, content hash, size, preview URL).",
        "security": [
          {
            "NamespaceCredential": []
          }
        ],
        "responses": {
          "200": {
            "description": "ok"
          },
          "401": {
            "description": "Missing or invalid namespace credential."
          },
          "404": {
            "description": "Project or frontend not found."
          }
        }
      }
    },
    "/api/v1/projects/{id}/frontends/{target}/revisions/{rid}/content": {
      "get": {
        "tags": [
          "project frontends"
        ],
        "summary": "Get one revision's stored source",
        "operationId": "getFrontendRevisionContent",
        "description": "Returns one revision's exact stored source. ?known_content_sha256= returns {unchanged:true} with no body when the caller's held hash still matches.",
        "security": [
          {
            "NamespaceCredential": []
          }
        ],
        "parameters": [
          {
            "in": "query",
            "name": "known_content_sha256",
            "schema": {
              "type": "string"
            },
            "description": "Conditional read."
          }
        ],
        "responses": {
          "200": {
            "description": "ok"
          },
          "401": {
            "description": "Missing or invalid namespace credential."
          },
          "404": {
            "description": "Project or frontend not found."
          }
        }
      }
    },
    "/api/v1/projects/{id}/frontends/{target}/revisions/{rid}/promote": {
      "post": {
        "tags": [
          "project frontends"
        ],
        "summary": "Promote a revision to the canonical URL",
        "operationId": "promoteFrontendRevision",
        "description": "Releases a draft revision to the canonical slug \u2014 the only way the revision workflow changes production. Re-renders the stored source through the production pipeline (runtime injection per the project's current capabilities) and publishes via the atomic swap. expected_active_revision_id is the optimistic-concurrency check.",
        "security": [
          {
            "NamespaceCredential": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "expected_active_revision_id": {
                    "type": "string",
                    "description": "The active revision the caller last saw \u2014 required; a stale one is a 409 conflict."
                  },
                  "message": {
                    "type": "string",
                    "description": "Optional audit note."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "ok"
          },
          "401": {
            "description": "Missing or invalid namespace credential."
          },
          "404": {
            "description": "Project or frontend not found."
          },
          "409": {
            "description": "Optimistic-concurrency conflict (revision_conflict): the release/draft base is stale."
          }
        }
      }
    },
    "/api/v1/projects/{id}/frontends/{target}/revisions/{rid}/rollback": {
      "post": {
        "tags": [
          "project frontends"
        ],
        "summary": "Roll back to a historic revision",
        "operationId": "rollbackFrontendRevision",
        "description": "Restores a formerly promoted revision to the canonical slug (the release event records the rollback; the displaced revision becomes rolled_back). Requires expected_active_revision_id as an optimistic-concurrency check.",
        "security": [
          {
            "NamespaceCredential": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "expected_active_revision_id": {
                    "type": "string",
                    "description": "The active revision the caller last saw \u2014 required; a stale one is a 409 conflict."
                  },
                  "message": {
                    "type": "string",
                    "description": "Optional audit note."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "ok"
          },
          "401": {
            "description": "Missing or invalid namespace credential."
          },
          "404": {
            "description": "Project or frontend not found."
          },
          "409": {
            "description": "Optimistic-concurrency conflict (revision_conflict): the release/draft base is stale."
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "DeployRequest": {
        "type": "object",
        "required": [
          "content"
        ],
        "properties": {
          "name": {
            "type": "string",
            "description": "Preferred slug. 3-63 chars, lowercase a-z, 0-9, hyphen, not starting/ending with hyphen. Reserved names rejected (api, www, mcp, admin, static, hostan, app, healthz, docs, assets, terms, privacy, acceptable-use, report). Auto-generated and suffixed on collision.",
            "minLength": 3,
            "maxLength": 63,
            "pattern": "^[a-z0-9]([a-z0-9\\-]*[a-z0-9])?$",
            "example": "my-app"
          },
          "content": {
            "type": "string",
            "description": "HTML, Markdown, Mermaid diagram source, JSON, or CSV. Must be non-empty, valid UTF-8, < 1 MB. Rendered HTML > 1 MB is also rejected.",
            "example": "<!DOCTYPE html><html><body>Hello!</body></html>"
          },
          "type": {
            "type": "string",
            "enum": [
              "html",
              "markdown",
              "mermaid",
              "json",
              "csv"
            ],
            "default": "html",
            "description": "How to interpret content. html=passthrough, markdown=goldmark+GFM\u2192paper layout, mermaid=interactive diagram page, json=interactive JSON explorer (searchable tree, collapse/expand, copy JSONPath), csv=interactive data explorer (filter, sort, charts). The historical \"database\" and \"app\" types are removed: databases and stateful apps are built as projects (manage_backend + manage_frontend over MCP) and are rejected with 400."
          },
          "title": {
            "type": "string",
            "description": "Page <title> and H1 heading for markdown/mermaid/json/csv. Auto-derived from first H1/slug if omitted (max 80 chars).",
            "example": "My Page"
          },
          "one_time": {
            "type": "boolean",
            "default": false,
            "description": "If true, self-destructs after first authorized visit (visit beacon \u2192 per-slug mutex \u2192 RemoveDeployment). 204 thereafter."
          },
          "password": {
            "type": "string",
            "description": "Optional access password. When set, viewers see a login page; password is PBKDF2-HMAC-SHA256 + server pepper (HOSTAN_PASSWORD_PEPPER) \u2014 never stored plaintext, never in URL. Requires pepper to be configured or deploy is rejected 400.",
            "example": "s3cret"
          }
        }
      },
      "DeployResponse": {
        "type": "object",
        "required": [
          "id",
          "slug",
          "url",
          "created_at",
          "expires_at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "example": "dep_01k8x7n1f8k0",
            "description": "Deployment ID (dep_ + 32 hex chars)"
          },
          "slug": {
            "type": "string",
            "example": "my-app"
          },
          "url": {
            "type": "string",
            "format": "uri",
            "example": "https://sites.hostan.app/my-app"
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "example": "2026-06-10T20:00:00Z"
          },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "example": "2026-06-17T20:00:00Z",
            "description": "Expiry of the quick-share (default TTL)."
          }
        }
      },
      "DBOpRequest": {
        "type": "object",
        "required": [
          "action"
        ],
        "properties": {
          "action": {
            "type": "string",
            "description": "Action name as defined by manage_backend update {actions} (lowercase identifier ^[a-z][a-z0-9_]{0,63}$). Unknown names answer 404 with the manage_backend get pointer."
          },
          "params": {
            "type": "object",
            "additionalProperties": true,
            "description": "Exactly the action's declared :name params \u2014 missing and extra keys are a 400 naming the diff. Values are JSON scalars bound as named params."
          }
        }
      },
      "DBQueryRequest": {
        "type": "object",
        "required": [
          "sql"
        ],
        "properties": {
          "sql": {
            "type": "string",
            "description": "Single SQL statement (SELECT/INSERT/UPDATE/DELETE, optional WITH prefix). Use ? placeholders."
          },
          "params": {
            "type": "array",
            "items": {
              "oneOf": [
                {
                  "type": "string"
                },
                {
                  "type": "integer"
                },
                {
                  "type": "number"
                },
                {
                  "type": "boolean"
                },
                {
                  "type": "null"
                }
              ]
            },
            "description": "Bind values for ? placeholders"
          }
        }
      },
      "DBQueryResponse": {
        "type": "object",
        "properties": {
          "columns": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Column names (SELECT only)"
          },
          "rows": {
            "type": "array",
            "items": {
              "type": "array"
            },
            "description": "Result rows (SELECT only)"
          },
          "truncated": {
            "type": "boolean",
            "description": "True if rows were capped at 500"
          },
          "changes": {
            "type": "integer",
            "description": "Rows affected (INSERT/UPDATE/DELETE)"
          },
          "last_insert_id": {
            "type": "integer",
            "description": "Last insert rowid (INSERT)"
          }
        }
      },
      "DBSchemaResponse": {
        "type": "object",
        "properties": {
          "tables": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/DBTable"
            }
          }
        }
      },
      "DBTable": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string"
          },
          "columns": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/DBColumn"
            }
          }
        }
      },
      "DBColumn": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string"
          },
          "type": {
            "type": "string"
          },
          "not_null": {
            "type": "boolean"
          },
          "primary_key": {
            "type": "boolean"
          },
          "has_default": {
            "type": "boolean"
          }
        }
      },
      "ErrorResponse": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string",
            "description": "Human-readable error message"
          }
        }
      },
      "ProjectView": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Project identity (UUIDv7)."
          },
          "name": {
            "type": "string",
            "description": "Project name."
          },
          "description": {
            "type": "string",
            "description": "Human-readable description; omitted when empty."
          },
          "namespace": {
            "type": "string",
            "description": "Owning namespace; empty only for the seeded free project (anonymous quick shares)."
          },
          "state": {
            "type": "string",
            "enum": [
              "active",
              "deleting"
            ],
            "description": "Lifecycle state."
          },
          "permanent": {
            "type": "boolean",
            "description": "True when the project has no expiry."
          },
          "expires_at": {
            "type": "string",
            "nullable": true,
            "description": "RFC3339 UTC expiry instant, or null when permanent. The project's clock is the only clock its resources answer to."
          },
          "created_at": {
            "type": "string",
            "description": "RFC3339 UTC creation instant."
          },
          "host_label": {
            "type": "string",
            "description": "The project's subdomain host label (#113): frontends are served at https://<host_label>.<sites domain>/<slug>. Unique platform-wide; empty only for the seeded free project."
          },
          "url": {
            "type": "string",
            "description": "The project's public origin (https://<host_label>.<sites domain>), when the platform's sites domain is configured."
          },
          "allowed_providers": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The stored per-project sign-in provider restriction (#275): absent (null) means every registered provider is allowed; a list restricts sign-in to exactly those methods."
          }
        }
      },
      "ProjectCreateRequest": {
        "type": "object",
        "required": [
          "name"
        ],
        "properties": {
          "name": {
            "type": "string",
            "description": "Project name (required)."
          },
          "description": {
            "type": "string",
            "description": "Human-readable description."
          },
          "ttl_seconds": {
            "type": "integer",
            "description": "Lifetime in seconds (optional). Mutually exclusive with permanent; omitted defaults to permanent."
          },
          "permanent": {
            "type": "boolean",
            "description": "Explicitly request a permanent project (optional; the default when ttl_seconds is omitted). Mutually exclusive with ttl_seconds."
          }
        }
      },
      "ProjectCreateResponse": {
        "type": "object",
        "properties": {
          "project": {
            "$ref": "#/components/schemas/ProjectView"
          },
          "docs": {
            "type": "string",
            "description": "Inline Markdown documentation of the project model (namespaces own projects; manage_* contracts)."
          }
        }
      },
      "ProjectListResponse": {
        "type": "object",
        "properties": {
          "projects": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ProjectView"
            }
          }
        }
      },
      "ProjectUpdateRequest": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "New name. Omit to leave unchanged; an empty string is rejected."
          },
          "description": {
            "type": "string",
            "description": "New description. Omit to leave unchanged; an empty string clears it."
          },
          "allowed_providers": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Per-project sign-in provider restriction (#275): the provider registry names this project's frontends may offer at sign-in. Present-but-empty clears the restriction (every registered provider); a non-empty list must name only registered providers (unknown names are a 400)."
          }
        }
      },
      "MemberView": {
        "type": "object",
        "properties": {
          "slug": {
            "type": "string"
          },
          "type": {
            "type": "string"
          },
          "state": {
            "type": "string"
          }
        }
      },
      "ProjectDetail": {
        "type": "object",
        "allOf": [
          {
            "$ref": "#/components/schemas/ProjectView"
          },
          {
            "type": "object",
            "properties": {
              "visits": {
                "type": "integer",
                "description": "Durable visit rollup over the project's deployments."
              },
              "members": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/MemberView"
                },
                "description": "The project's frontend inventory."
              }
            }
          }
        ]
      },
      "FrontendCreateRequest": {
        "type": "object",
        "required": [
          "content"
        ],
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "html",
              "markdown",
              "mermaid",
              "json",
              "csv"
            ],
            "description": "Content type; omitted defaults to html. Fixed at create \u2014 updates republish the same type."
          },
          "content": {
            "type": "string",
            "description": "The content to publish."
          },
          "requested_slug": {
            "type": "string",
            "description": "Preferred global slug (lowercase letters, digits, hyphens; 3-63 chars). On collision a suffixed variant is chosen automatically."
          },
          "title": {
            "type": "string",
            "description": "Optional page title."
          }
        }
      },
      "FrontendDeployResponse": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "The deployment's immutable id (UUIDv7)."
          },
          "slug": {
            "type": "string"
          },
          "url": {
            "type": "string",
            "description": "The public page URL — the project subdomain form (https://<host_label>.<sites domain>/<slug>) when the sites domain is configured; the shared sites origin stays the storage/edge identity and keeps serving the page (#283)."
          },
          "api_base": {
            "type": "string",
            "description": "The frontend's tokenless query base (<url>/api) \u2014 resolves to the project's backend when one exists, 404 otherwise."
          },
          "project_id": {
            "type": "string",
            "description": "The owning project."
          },
          "created_at": {
            "type": "string",
            "description": "RFC3339 UTC."
          },
          "expires_at": {
            "type": "string",
            "nullable": true,
            "description": "The project's clock, or null when the project is permanent."
          }
        }
      },
      "FrontendMetadata": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "slug": {
            "type": "string"
          },
          "url": {
            "type": "string",
            "description": "The public page URL — the project subdomain form when the sites domain is configured (#283); the shared origin stays valid."
          },
          "type": {
            "type": "string"
          },
          "state": {
            "type": "string"
          },
          "revision": {
            "type": "integer",
            "description": "0 = original, bumped per published update."
          },
          "visits": {
            "type": "integer"
          },
          "created_at": {
            "type": "string"
          },
          "updated_at": {
            "type": "string",
            "description": "Present after the first update."
          },
          "expires_at": {
            "type": "string",
            "nullable": true
          },
          "permanent": {
            "type": "boolean"
          }
        }
      },
      "FrontendListResponse": {
        "type": "object",
        "properties": {
          "frontends": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/FrontendMetadata"
            }
          }
        }
      },
      "FrontendUpdateRequest": {
        "type": "object",
        "required": [
          "content"
        ],
        "properties": {
          "content": {
            "type": "string",
            "description": "The new content."
          },
          "type": {
            "type": "string",
            "description": "Optional; must match the existing type (fixed at create)."
          },
          "title": {
            "type": "string",
            "description": "Optional page title; republishing without one drops the title."
          },
          "requested_slug": {
            "type": "string",
            "description": "Not accepted on update: a non-empty value answers 400 (the slug is immutable) instead of being silently ignored. Create-only field.",
            "deprecated": true
          }
        }
      },
      "BackendCreateRequest": {
        "type": "object",
        "required": [
          "ddl"
        ],
        "properties": {
          "ddl": {
            "type": "string",
            "description": "Provisioning DDL: CREATE TABLE/INDEX/VIEW plus optional seed INSERTs; at least one CREATE TABLE."
          },
          "realtime": {
            "type": "boolean",
            "default": false,
            "description": "Opt the new backend into the real-time capability (#150): the project-scoped WebSocket endpoint goes live and frontends published from then on embed the hostan.ws runtime."
          }
        }
      },
      "DatabaseView": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "The database resource id (the private runtime directory key)."
          },
          "name": {
            "type": "string"
          }
        }
      },
      "BackendView": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "project_id": {
            "type": "string"
          },
          "spec_version": {
            "type": "integer"
          },
          "state": {
            "type": "string"
          },
          "database": {
            "$ref": "#/components/schemas/DatabaseView"
          },
          "realtime": {
            "type": "boolean",
            "description": "The opt-in real-time capability (#150): when true, the project exposes its WebSocket room relay at /<frontend-slug>/ws and frontends published from then on embed the hostan.ws runtime."
          }
        }
      },
      "BackendMigrationRequest": {
        "type": "object",
        "required": [
          "ddl"
        ],
        "properties": {
          "ddl": {
            "type": "string",
            "description": "ONE additive migration."
          },
          "note": {
            "type": "string",
            "description": "Optional note recorded with the ledger entry (what and why)."
          },
          "realtime": {
            "type": "boolean",
            "description": "Realtime capability flip (#150). Send alone \u2014 never combined with ddl: true enables the project's WebSocket endpoint for new upgrades, false disables it and closes live sockets. An already-applied flip is idempotent."
          }
        }
      },
      "MigrationResult": {
        "type": "object",
        "properties": {
          "seq": {
            "type": "integer",
            "description": "Ledger sequence (0 is the provisioning DDL)."
          },
          "checksum": {
            "type": "string",
            "description": "SHA-256 of the normalized SQL."
          },
          "note": {
            "type": "string"
          },
          "already_applied": {
            "type": "boolean",
            "description": "True on an idempotent retry \u2014 nothing was re-run."
          },
          "healed": {
            "type": "boolean",
            "description": "True when this call recorded a lagged ledger row (the crash window)."
          }
        }
      },
      "ColumnInfo": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string"
          },
          "type": {
            "type": "string"
          },
          "not_null": {
            "type": "boolean"
          },
          "primary_key": {
            "type": "boolean"
          },
          "has_default": {
            "type": "boolean"
          }
        }
      },
      "TableInfo": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string"
          },
          "columns": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ColumnInfo"
            }
          }
        }
      },
      "ActionView": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string"
          },
          "version": {
            "type": "integer",
            "description": "Ledger version of this action definition."
          },
          "checksum": {
            "type": "string"
          },
          "params": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The strict invocation contract: an op call must provide exactly these :name binds."
          },
          "note": {
            "type": "string"
          },
          "updated_at": {
            "type": "string"
          }
        }
      },
      "BackendDetail": {
        "type": "object",
        "properties": {
          "backend": {
            "$ref": "#/components/schemas/BackendView"
          },
          "spec": {
            "type": "object",
            "properties": {
              "version": {
                "type": "integer"
              },
              "database": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "engine": {
                    "type": "string"
                  },
                  "max_bytes": {
                    "type": "integer"
                  }
                }
              }
            }
          },
          "schema": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/TableInfo"
            },
            "description": "Derived live from the database file."
          },
          "migrations": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "seq": {
                  "type": "integer"
                },
                "checksum": {
                  "type": "string"
                },
                "note": {
                  "type": "string"
                },
                "applied_at": {
                  "type": "string"
                }
              }
            }
          },
          "actions": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ActionView"
            }
          },
          "inventory": {
            "type": "object",
            "properties": {
              "tables": {
                "type": "integer"
              },
              "size_bytes": {
                "type": "integer"
              }
            }
          },
          "warnings": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Drift warnings (e.g. schema_ahead_of_ledger \u2014 heal by re-sending the migration)."
          }
        }
      },
      "BackendActionInput": {
        "type": "object",
        "required": [
          "name",
          "sql"
        ],
        "properties": {
          "name": {
            "type": "string",
            "description": "Invocation name (lowercase letters/digits/underscores, starts with a letter)."
          },
          "sql": {
            "type": "string",
            "description": "One SELECT/INSERT/UPDATE/DELETE statement, :name binds only."
          },
          "note": {
            "type": "string",
            "description": "Optional; immutable per version."
          }
        }
      },
      "BackendActionsRequest": {
        "type": "object",
        "required": [
          "actions"
        ],
        "properties": {
          "actions": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/BackendActionInput"
            },
            "minItems": 1
          }
        }
      },
      "BackendActionsResponse": {
        "type": "object",
        "properties": {
          "actions": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ActionView"
            }
          }
        }
      },
      "PaymentsAddPriceRequest": {
        "type": "object",
        "required": [
          "product_name",
          "label",
          "amount",
          "currency"
        ],
        "properties": {
          "product_name": {
            "type": "string",
            "description": "The product the price belongs to (created implicitly)."
          },
          "description": {
            "type": "string",
            "description": "Optional product description."
          },
          "label": {
            "type": "string",
            "description": "The checkout line-item name visitors see."
          },
          "amount": {
            "type": "integer",
            "description": "Positive integer in minor units (cents for usd). Never client-computed on the visitor side."
          },
          "currency": {
            "type": "string",
            "description": "3-letter ISO 4217 code, lowercased (e.g. usd)."
          },
          "recurring": {
            "type": "boolean",
            "description": "Marks the price recurring: bills as a monthly subscription. Omitted/false is a one-time payment."
          }
        }
      },
      "PaymentsPrice": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "price_\u2026 \u2014 the only catalog identifier a visitor page handles."
          },
          "product_id": {
            "type": "string"
          },
          "label": {
            "type": "string"
          },
          "amount": {
            "type": "integer",
            "description": "Minor units."
          },
          "currency": {
            "type": "string"
          },
          "recurring": {
            "type": "boolean"
          },
          "active": {
            "type": "boolean"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "PaymentsCatalog": {
        "type": "object",
        "properties": {
          "products": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PaymentsProduct"
            }
          },
          "prices": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PaymentsPrice"
            }
          }
        }
      },
      "PaymentsProduct": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "provider_id": {
            "type": "string",
            "description": "The provider object id, lazily created on first checkout."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "PaymentsSessions": {
        "type": "object",
        "properties": {
          "sessions": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PaymentsSession"
            }
          }
        }
      },
      "PaymentsSession": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "pay_\u2026 \u2014 the platform checkout session id."
          },
          "state": {
            "type": "string",
            "enum": [
              "open",
              "paid",
              "canceled",
              "failed",
              "refunded"
            ]
          },
          "price_id": {
            "type": "string"
          },
          "label": {
            "type": "string"
          },
          "amount": {
            "type": "integer"
          },
          "currency": {
            "type": "string"
          },
          "user_id": {
            "type": "string",
            "description": "usr_\u2026 when identity was active at checkout."
          },
          "email": {
            "type": "string",
            "description": "Provider-collected buyer email for anonymous checkouts."
          },
          "event_id": {
            "type": "string",
            "description": "The verified webhook event that produced the current verdict."
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "PaymentsDeactivateResult": {
        "type": "object",
        "properties": {
          "deactivated": {
            "type": "boolean"
          }
        }
      }
    },
    "responses": {
      "BadRequest": {
        "description": "Validation error (invalid slug, empty content, bad type, missing pepper, rendered payload too large, invalid JSON)",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            }
          }
        }
      },
      "PayloadTooLarge": {
        "description": "Body > 1 MB (nginx client_max_body_size + app MaxBytesReader)",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            }
          }
        }
      },
      "RateLimited": {
        "description": "Rate limit exceeded (429 + Retry-After: 60)",
        "headers": {
          "Retry-After": {
            "schema": {
              "type": "string"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            }
          }
        }
      },
      "Capacity": {
        "description": "Service at capacity (5000 active or <100 MB free) \u2014 503 + Retry-After: 60 with reason",
        "headers": {
          "Retry-After": {
            "schema": {
              "type": "string",
              "example": "60"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            }
          }
        }
      },
      "InternalError": {
        "description": "Storage failure (500, generic message \u2014 raw error never leaked)",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            }
          }
        }
      },
      "Unauthorized": {
        "description": "Missing, invalid, or revoked credential."
      },
      "Forbidden": {
        "description": "The credential does not own this deployment."
      },
      "NotFound": {
        "description": "Deployment not found."
      },
      "Conflict": {
        "description": "A deterministic create with an existing slug."
      }
    },
    "securitySchemes": {
      "NamespaceCredential": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "hstn_<id>_<secret>",
        "description": "The admin-issued namespace credential (hstn_\u2026), the only platform credential. Required for POST/GET /api/v1/projects; resolves to the owning namespace. POST /api/v1/deploy is anonymous quick-share only and takes no credential."
      }
    }
  },
  "externalDocs": {
    "description": "Full docs",
    "url": "https://hostan.app/docs"
  }
}