{
  "openapi": "3.0.3",
  "info": {
    "title": "Simple Host API",
    "description": "Static website hosting with a light per-site backend. Deploy a site with one API call \u2014 inline JSON files (LLM-friendly) or a tar.gz/zip archive \u2014 and each site gets shared JSON state (with atomic ops), append-only collections, optional password (view-lock) protection, and starter templates. Sign-in is a 6-digit email code that returns an API key, sent as the `X-API-Key` header.",
    "version": "2.0.0"
  },
  "servers": [
    {
      "url": "https://simple-host.app",
      "description": "Production"
    }
  ],
  "tags": [
    {
      "name": "Auth",
      "description": "Email-code sign-in"
    },
    {
      "name": "Deploy",
      "description": "Create/update a site (JSON files or archive)"
    },
    {
      "name": "Sites",
      "description": "List, versions, rollback, delete"
    },
    {
      "name": "State",
      "description": "Per-site shared JSON store (public, Origin-gated)"
    },
    {
      "name": "Collections",
      "description": "Append-only per-site lists"
    },
    {
      "name": "Private pages",
      "description": "Password (view-lock) protection"
    },
    {
      "name": "Templates",
      "description": "Starter templates"
    },
    {
      "name": "AI",
      "description": "Generate a site from a prompt"
    },
    {
      "name": "Health"
    }
  ],
  "paths": {
    "/v1/auth": {
      "post": {
        "operationId": "postAuth",
        "summary": "Request a sign-in code",
        "description": "Emails a 6-digit code (and a magic link) to the address. No user row is created here \u2014 the account is created lazily on the first successful verify, so this can't be used to enumerate users.",
        "tags": [
          "Auth"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "email"
                ],
                "properties": {
                  "email": {
                    "type": "string",
                    "format": "email",
                    "example": "you@example.com"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Code sent",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "message": {
                      "type": "string"
                    },
                    "email": {
                      "type": "string"
                    },
                    "expires_in_seconds": {
                      "type": "integer",
                      "example": 900
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/v1/auth/verify": {
      "post": {
        "operationId": "postAuthVerify",
        "summary": "Verify the code and get an API key",
        "description": "Exchange `{email, code}` (CLI/agent) or a magic-link `{token}` (browser) for an API key. The first successful verify creates the account. The code expires in 15 minutes and allows 3 attempts.",
        "tags": [
          "Auth"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "email": {
                    "type": "string",
                    "format": "email"
                  },
                  "code": {
                    "type": "string",
                    "example": "123456"
                  },
                  "token": {
                    "type": "string",
                    "description": "magic-link token (alternative to email+code)"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Authenticated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AuthResponse"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "description": "Invalid or expired code",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/me": {
      "get": {
        "operationId": "getMe",
        "summary": "Current user",
        "tags": [
          "Auth"
        ],
        "security": [
          {
            "apiKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "Current user",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "username": {
                      "type": "string"
                    },
                    "is_admin": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/v1/sites": {
      "get": {
        "operationId": "getSites",
        "summary": "List your sites",
        "description": "Returns the authenticated user's sites. Admins see all sites.",
        "tags": [
          "Sites"
        ],
        "security": [
          {
            "apiKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "List of sites",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Site"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/v1/sites/{sitename}/files": {
      "post": {
        "operationId": "postSitesBySitenameFiles",
        "summary": "Deploy a site from inline JSON files",
        "description": "The LLM-friendly deploy path \u2014 send every file inline in one request, no archiving. `index.html` is required. Relative paths only (`..`/absolute are rejected); secret files (`.env`, `.git/*`, `id_rsa`) are dropped and script extensions (`.sh .py .php \u2026`) are rejected. Use PUT to update an existing site.",
        "tags": [
          "Deploy"
        ],
        "security": [
          {
            "apiKey": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/sitename"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/FilesBody"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Site created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SiteWithNote"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          }
        }
      },
      "put": {
        "operationId": "putSitesBySitenameFiles",
        "summary": "Update a site from inline JSON files",
        "description": "Same as POST, but for an existing site you own. Creates a new version and activates it.",
        "tags": [
          "Deploy"
        ],
        "security": [
          {
            "apiKey": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/sitename"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/FilesBody"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Site updated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SiteWithNote"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/sites/{sitename}": {
      "post": {
        "operationId": "postSitesBySitename",
        "summary": "Create a site from an archive",
        "description": "Upload a `.tar.gz` or `.zip` (for framework builds, binary assets, large sites). Max 100 MB.",
        "tags": [
          "Deploy"
        ],
        "security": [
          {
            "apiKey": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/sitename"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/gzip": {
              "schema": {
                "type": "string",
                "format": "binary"
              }
            },
            "application/zip": {
              "schema": {
                "type": "string",
                "format": "binary"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Site created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SiteWithNote"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "413": {
            "$ref": "#/components/responses/TooLarge"
          }
        }
      },
      "put": {
        "operationId": "putSitesBySitename",
        "summary": "Update a site from an archive",
        "description": "Upload a new version of an existing site you own. Creates a new version and activates it.",
        "tags": [
          "Deploy"
        ],
        "security": [
          {
            "apiKey": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/sitename"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/gzip": {
              "schema": {
                "type": "string",
                "format": "binary"
              }
            },
            "application/zip": {
              "schema": {
                "type": "string",
                "format": "binary"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Site updated",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SiteWithNote"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "413": {
            "$ref": "#/components/responses/TooLarge"
          }
        }
      },
      "delete": {
        "operationId": "deleteSitesBySitename",
        "summary": "Delete your site",
        "description": "Deletes the site you own \u2014 all versions and files from disk.",
        "tags": [
          "Sites"
        ],
        "security": [
          {
            "apiKey": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/sitename"
          }
        ],
        "responses": {
          "204": {
            "description": "Site deleted"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/sites/{sitename}/versions": {
      "get": {
        "operationId": "getSitesBySitenameVersions",
        "summary": "List a site's versions",
        "tags": [
          "Sites"
        ],
        "security": [
          {
            "apiKey": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/sitename"
          }
        ],
        "responses": {
          "200": {
            "description": "Versions (newest first)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "properties": {
                      "version_number": {
                        "type": "integer"
                      },
                      "created_at": {
                        "type": "string",
                        "format": "date-time"
                      },
                      "is_active": {
                        "type": "boolean"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/sites/{sitename}/active-version": {
      "put": {
        "operationId": "putSitesBySitenameActiveVersion",
        "summary": "Roll back to a version",
        "description": "Re-point the live site at an existing version number.",
        "tags": [
          "Sites"
        ],
        "security": [
          {
            "apiKey": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/sitename"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "version_number"
                ],
                "properties": {
                  "version_number": {
                    "type": "integer",
                    "example": 2
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Active version changed"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/sites/{sitename}/state": {
      "get": {
        "operationId": "getSitesBySitenameState",
        "summary": "Read per-site JSON state",
        "description": "Returns the JSON blob for this site (`null` if unset). PUBLIC store \u2014 no API key; the server checks `Origin`/`Referer` and only accepts calls from the site's own page (`https://{sitename}.simple-host.app`). The response carries an `ETag`; send `If-None-Match: <etag>` to get a `304` when nothing changed (cheap polling). Never store secrets here.",
        "tags": [
          "State"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/sitename"
          },
          {
            "name": "If-None-Match",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Current state (arbitrary JSON, `null` if unset)",
            "headers": {
              "ETag": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {}
              }
            }
          },
          "304": {
            "description": "Not modified (matched If-None-Match)"
          },
          "403": {
            "$ref": "#/components/responses/OriginMismatch"
          }
        }
      },
      "put": {
        "operationId": "putSitesBySitenameState",
        "summary": "Replace per-site JSON state",
        "description": "Overwrites the whole blob (\u2264 1 MB), last-write-wins. Optionally pass `If-Match: <etag>` for optimistic concurrency (returns 412 on conflict). Origin-gated, no API key.",
        "tags": [
          "State"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/sitename"
          },
          {
            "name": "If-Match",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {}
            }
          }
        },
        "responses": {
          "200": {
            "description": "Saved; echoes the value",
            "headers": {
              "ETag": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {}
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "403": {
            "$ref": "#/components/responses/OriginMismatch"
          },
          "412": {
            "description": "ETag did not match (If-Match)"
          },
          "413": {
            "$ref": "#/components/responses/TooLarge"
          }
        }
      },
      "patch": {
        "operationId": "patchSitesBySitenameState",
        "summary": "Atomically update per-site state",
        "description": "Apply one or more atomic operations so concurrent writers never clobber. Origin-gated, no API key. Ops \u2014 `set` (path,value), `inc` (path,by), `append` (path,value), `remove` (path), `removeWhere` (path,match).",
        "tags": [
          "State"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/sitename"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "ops"
                ],
                "properties": {
                  "ops": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "op": {
                          "type": "string",
                          "enum": [
                            "set",
                            "inc",
                            "append",
                            "remove",
                            "removeWhere"
                          ]
                        },
                        "path": {
                          "type": "string",
                          "example": "counters.visits"
                        },
                        "value": {},
                        "by": {
                          "type": "number"
                        },
                        "match": {
                          "type": "object"
                        }
                      }
                    }
                  }
                },
                "example": {
                  "ops": [
                    {
                      "op": "inc",
                      "path": "count",
                      "by": 1
                    },
                    {
                      "op": "append",
                      "path": "items",
                      "value": {
                        "id": "x"
                      }
                    }
                  ]
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "New state",
            "headers": {
              "ETag": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {}
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "403": {
            "$ref": "#/components/responses/OriginMismatch"
          }
        }
      }
    },
    "/v1/sites/{sitename}/collections/{coll}": {
      "post": {
        "operationId": "postSitesBySitenameCollectionsByColl",
        "summary": "Append an item to a collection",
        "description": "Append-only list (O(1) insert). For signups, RSVPs, submissions. Origin-gated, no API key. Each item \u2264 64 KB.",
        "tags": [
          "Collections"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/sitename"
          },
          {
            "$ref": "#/components/parameters/coll"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Item appended"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "403": {
            "$ref": "#/components/responses/OriginMismatch"
          }
        }
      },
      "get": {
        "operationId": "getSitesBySitenameCollectionsByColl",
        "summary": "List items in a collection",
        "description": "Newest-first, paginated. Origin-gated, no API key.",
        "tags": [
          "Collections"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/sitename"
          },
          {
            "$ref": "#/components/parameters/coll"
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 50,
              "maximum": 200
            }
          },
          {
            "name": "before",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "cursor for the next page"
          }
        ],
        "responses": {
          "200": {
            "description": "Items",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/OriginMismatch"
          }
        }
      }
    },
    "/v1/sites/{sitename}/view-password": {
      "put": {
        "operationId": "putSitesBySitenameViewPassword",
        "summary": "Set a view password (make the site private)",
        "description": "Password-protect the whole site (view-lock). Visitors get a custom login page + signed cookie, and the locked page's state/collections also require unlocking. Owner only (API key).",
        "tags": [
          "Private pages"
        ],
        "security": [
          {
            "apiKey": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/sitename"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "password"
                ],
                "properties": {
                  "password": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "204": {
            "description": "View password set"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "delete": {
        "operationId": "deleteSitesBySitenameViewPassword",
        "summary": "Remove the view password (make the site public again)",
        "tags": [
          "Private pages"
        ],
        "security": [
          {
            "apiKey": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/sitename"
          }
        ],
        "responses": {
          "204": {
            "description": "View password removed"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/templates": {
      "get": {
        "operationId": "getTemplates",
        "summary": "List starter templates",
        "description": "Public. Returns id + title + description for each template.",
        "tags": [
          "Templates"
        ],
        "responses": {
          "200": {
            "description": "Templates",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/TemplateMeta"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/templates/{id}": {
      "get": {
        "operationId": "getTemplatesById",
        "summary": "Get a template's deployable files",
        "description": "Public. Returns the template's `files` map, ready to POST to `/v1/sites/{name}/files`.",
        "tags": [
          "Templates"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "landing"
          }
        ],
        "responses": {
          "200": {
            "description": "Template with files",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/TemplateMeta"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "files": {
                          "type": "object",
                          "additionalProperties": {
                            "type": "string"
                          }
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/v1/generate": {
      "post": {
        "operationId": "postGenerate",
        "summary": "Generate a site from a prompt (AI create)",
        "description": "Powers the home-page \"create with AI\" chat. Sign-in-gated and rate-limited (spends Anthropic credits). Send the conversation and (optionally) the current HTML and attachments; returns a short reply and, when ready, the HTML. Disabled if the server has no Anthropic key.",
        "tags": [
          "AI"
        ],
        "security": [
          {
            "apiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "messages": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "role": {
                          "type": "string",
                          "enum": [
                            "user",
                            "assistant"
                          ]
                        },
                        "content": {
                          "type": "string"
                        }
                      }
                    }
                  },
                  "html": {
                    "type": "string",
                    "description": "current site HTML, for incremental edits"
                  },
                  "attachments": {
                    "type": "array",
                    "description": "images / PDFs / text on the latest user turn",
                    "items": {
                      "type": "object",
                      "properties": {
                        "kind": {
                          "type": "string",
                          "enum": [
                            "image",
                            "document",
                            "text"
                          ]
                        },
                        "mediaType": {
                          "type": "string"
                        },
                        "name": {
                          "type": "string"
                        },
                        "data": {
                          "type": "string",
                          "description": "base64 (image/document)"
                        },
                        "text": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "With an agent backend, returns a jobId to poll at /v1/generate/status. With the direct backend, returns the reply (+ HTML when the model builds/updates the site) inline.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "jobId": {
                      "type": "string",
                      "description": "poll /v1/generate/status with this (async backend)"
                    },
                    "reply": {
                      "type": "string"
                    },
                    "html": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/v1/generate/status": {
      "get": {
        "operationId": "getGenerateStatus",
        "summary": "Poll an async AI-create job",
        "description": "Poll the status of a job started by POST /v1/generate on the agent backend. Returns running until the agent finishes, then done with the reply and HTML. Sign-in-gated; the job is bound to the caller's key.",
        "tags": [
          "AI"
        ],
        "security": [
          {
            "apiKey": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "jobId from POST /v1/generate"
          }
        ],
        "responses": {
          "200": {
            "description": "Job status",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "enum": [
                        "running",
                        "done",
                        "error"
                      ]
                    },
                    "reply": {
                      "type": "string"
                    },
                    "html": {
                      "type": "string"
                    },
                    "error": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "Unknown or expired job"
          }
        }
      }
    },
    "/healthz": {
      "get": {
        "operationId": "getHealthz",
        "summary": "Liveness probe",
        "tags": [
          "Health"
        ],
        "responses": {
          "200": {
            "description": "Alive",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "example": "ok"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/readyz": {
      "get": {
        "operationId": "getReadyz",
        "summary": "Readiness probe (DB connected)",
        "tags": [
          "Health"
        ],
        "responses": {
          "200": {
            "description": "Ready",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "example": "ok"
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "Not ready"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "apiKey": {
        "type": "apiKey",
        "in": "header",
        "name": "X-API-Key"
      }
    },
    "parameters": {
      "sitename": {
        "name": "sitename",
        "in": "path",
        "required": true,
        "schema": {
          "type": "string"
        },
        "example": "my-cool-site"
      },
      "coll": {
        "name": "coll",
        "in": "path",
        "required": true,
        "schema": {
          "type": "string"
        },
        "example": "signups"
      }
    },
    "schemas": {
      "AuthResponse": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "username": {
            "type": "string"
          },
          "api_key": {
            "type": "string"
          },
          "is_admin": {
            "type": "boolean"
          },
          "created": {
            "type": "boolean",
            "description": "true if this verify created the account"
          }
        }
      },
      "FilesBody": {
        "type": "object",
        "required": [
          "files"
        ],
        "properties": {
          "files": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            },
            "description": "relative path -> file contents (text). `index.html` required."
          }
        },
        "example": {
          "files": {
            "index.html": "<!DOCTYPE html><h1>Hello</h1>",
            "css/style.css": "body{font-family:sans-serif}"
          }
        }
      },
      "Site": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "user_id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string"
          },
          "active_version": {
            "type": "integer"
          },
          "site_url": {
            "type": "string",
            "nullable": true,
            "example": "https://my-cool-site.simple-host.app"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "SiteWithNote": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Site"
          },
          {
            "type": "object",
            "properties": {
              "note": {
                "type": "string"
              }
            }
          }
        ]
      },
      "TemplateMeta": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "example": "landing"
          },
          "title": {
            "type": "string"
          },
          "description": {
            "type": "string"
          }
        }
      },
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string"
          }
        }
      }
    },
    "responses": {
      "BadRequest": {
        "description": "Invalid request",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Unauthorized": {
        "description": "Missing or invalid API key \u2014 send the X-API-Key header, not Authorization Bearer",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Forbidden": {
        "description": "Admin access required",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "NotFound": {
        "description": "Not found",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Conflict": {
        "description": "Site name already exists",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "TooLarge": {
        "description": "Payload too large",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "TooManyRequests": {
        "description": "Rate limited",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "OriginMismatch": {
        "description": "Origin/Referer does not match the site (or the site is locked)",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    }
  }
}