{
  "openapi": "3.1.0",
  "info": {
    "title": "instanode.dev",
    "version": "0.1.0",
    "description": "AI-native infrastructure provisioning. Databases and webhooks in one HTTP call.",
    "contact": {
      "name": "instanode.dev",
      "url": "https://instanode.dev"
    }
  },
  "servers": [
    {
      "url": "https://api.instanode.dev",
      "description": "Production"
    }
  ],
  "tags": [
    {"name": "provisioning", "description": "Create anonymous databases and webhook receivers."},
    {"name": "webhook", "description": "Inbound webhook receiver."},
    {"name": "auth", "description": "GitHub OAuth session entry points."},
    {"name": "dashboard", "description": "Authenticated resource management."},
    {"name": "meta", "description": "Health and machine-readable docs."}
  ],
  "paths": {
    "/healthz": {
      "get": {
        "tags": ["meta"],
        "summary": "Health check",
        "description": "Liveness probe. Always returns 200 when the process is running.",
        "operationId": "healthz",
        "responses": {
          "200": {
            "description": "Service is up.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["ok", "service"],
                  "properties": {
                    "ok":      {"type": "boolean", "example": true},
                    "service": {"type": "string",  "example": "instant-lite"}
                  }
                }
              }
            }
          }
        }
      }
    },
    "/db/new": {
      "post": {
        "tags": ["provisioning"],
        "summary": "Provision a Postgres database",
        "description": "Creates a fresh Postgres database with pgvector. `name` is required — it's the human label surfaced in the dashboard. Anonymous callers (no session cookie, no bearer token) get 5 provisions/subnet/day and 24h-TTL resources. Authenticated paid callers (session cookie OR `Authorization: Bearer <JWT>` minted at /api/me/token) get permanent resources with no per-subnet cap.",
        "operationId": "newDB",
        "security": [{}, {"sessionCookie": []}, {"bearerAuth": []}],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {"$ref": "#/components/schemas/ProvisionRequest"}
            }
          }
        },
        "responses": {
          "201": {
            "description": "A new database was provisioned.",
            "content": {
              "application/json": {
                "schema": {"$ref": "#/components/schemas/ProvisionResponse"}
              }
            }
          },
          "200": {
            "description": "Daily provision limit already reached; returning the caller's existing resource.",
            "content": {
              "application/json": {
                "schema": {"$ref": "#/components/schemas/ProvisionResponse"}
              }
            }
          },
          "429": {
            "description": "Daily provision limit reached and no existing resource was found.",
            "content": {
              "application/json": {
                "schema": {"$ref": "#/components/schemas/RateLimitedError"}
              }
            }
          },
          "503": {
            "description": "Provisioning backend was unavailable.",
            "content": {
              "application/json": {
                "schema": {"$ref": "#/components/schemas/Error"}
              }
            }
          }
        }
      }
    },
    "/webhook/new": {
      "post": {
        "tags": ["provisioning"],
        "summary": "Provision a webhook receiver",
        "description": "Returns a `receive_url` that accepts arbitrary payloads. `name` is required — it's the human label surfaced in the dashboard. Same auth rules as /db/new: anonymous → 5/subnet/day + 24h TTL, authenticated paid → permanent with no subnet cap.",
        "operationId": "newWebhook",
        "security": [{}, {"sessionCookie": []}, {"bearerAuth": []}],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {"$ref": "#/components/schemas/ProvisionRequest"}
            }
          }
        },
        "responses": {
          "201": {
            "description": "A new webhook receiver was provisioned.",
            "content": {
              "application/json": {
                "schema": {"$ref": "#/components/schemas/WebhookProvisionResponse"}
              }
            }
          },
          "200": {
            "description": "Daily provision limit already reached; returning the caller's existing resource.",
            "content": {
              "application/json": {
                "schema": {"$ref": "#/components/schemas/WebhookProvisionResponse"}
              }
            }
          },
          "429": {
            "description": "Daily provision limit reached and no existing resource was found.",
            "content": {
              "application/json": {
                "schema": {"$ref": "#/components/schemas/RateLimitedError"}
              }
            }
          },
          "503": {
            "description": "Provisioning backend was unavailable.",
            "content": {
              "application/json": {
                "schema": {"$ref": "#/components/schemas/Error"}
              }
            }
          }
        }
      }
    },
    "/webhook/receive/{token}": {
      "parameters": [
        {
          "name": "token",
          "in": "path",
          "required": true,
          "description": "Webhook receiver token from `POST /webhook/new`.",
          "schema": {"type": "string", "format": "uuid"}
        }
      ],
      "post": {
        "tags": ["webhook"],
        "summary": "Deliver a webhook payload",
        "description": "Stores the inbound request (method, headers, body) in Redis keyed by the webhook token. Accepts any content type.",
        "operationId": "receiveWebhookPost",
        "requestBody": {
          "required": false,
          "description": "Arbitrary payload. Capped by the server's `webhook_max_body_bytes` limit.",
          "content": {
            "*/*": {"schema": {}}
          }
        },
        "responses": {
          "200": {
            "description": "Request stored.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["ok", "id"],
                  "properties": {
                    "ok": {"type": "boolean"},
                    "id": {"type": "string", "format": "uuid", "description": "Generated identifier for this stored request."}
                  }
                }
              }
            }
          },
          "400": {
            "description": "Malformed token.",
            "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}
          },
          "404": {
            "description": "No webhook resource exists for this token.",
            "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}
          },
          "410": {
            "description": "Webhook is no longer active (expired or deleted).",
            "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}
          },
          "503": {
            "description": "Failed to look up the webhook resource.",
            "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}
          }
        }
      },
      "get": {
        "tags": ["webhook"],
        "summary": "Deliver a GET-style webhook",
        "description": "Identical to the POST variant — some providers (e.g. health pings) deliver via GET. Stores method, headers, and empty body.",
        "operationId": "receiveWebhookGet",
        "responses": {
          "200": {
            "description": "Request stored.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["ok", "id"],
                  "properties": {
                    "ok": {"type": "boolean"},
                    "id": {"type": "string", "format": "uuid"}
                  }
                }
              }
            }
          },
          "400": {"description": "Malformed token.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}},
          "404": {"description": "No webhook resource exists for this token.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}},
          "410": {"description": "Webhook is no longer active.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}}
        }
      }
    },
    "/auth/github/login": {
      "get": {
        "tags": ["auth"],
        "summary": "Start GitHub OAuth flow",
        "description": "Sets an `oauth_state` cookie and 302-redirects to GitHub's authorization endpoint.",
        "operationId": "githubLogin",
        "responses": {
          "302": {
            "description": "Redirect to GitHub authorization URL.",
            "headers": {
              "Location": {
                "description": "`https://github.com/login/oauth/authorize?...`",
                "schema": {"type": "string", "format": "uri"}
              },
              "Set-Cookie": {
                "description": "`oauth_state` — random 32-hex-char value used to verify the callback.",
                "schema": {"type": "string"}
              }
            }
          }
        }
      }
    },
    "/auth/me": {
      "get": {
        "tags": ["auth"],
        "summary": "Current authenticated user",
        "description": "Returns the user represented by the `session` cookie.",
        "operationId": "me",
        "security": [{"sessionCookie": []}],
        "responses": {
          "200": {
            "description": "Authenticated user.",
            "content": {
              "application/json": {"schema": {"$ref": "#/components/schemas/User"}}
            }
          },
          "401": {"description": "No valid session cookie."}
        }
      }
    },
    "/auth/logout": {
      "post": {
        "tags": ["auth"],
        "summary": "Log out",
        "description": "Expires the `session` cookie and redirects to `/`.",
        "operationId": "logout",
        "responses": {
          "302": {
            "description": "Session cleared; redirect to `/`.",
            "headers": {
              "Location":   {"schema": {"type": "string", "format": "uri"}},
              "Set-Cookie": {"description": "`session=; MaxAge=-1`.", "schema": {"type": "string"}}
            }
          }
        }
      }
    },
    "/api/me/resources": {
      "get": {
        "tags": ["dashboard"],
        "summary": "List the authenticated user's resources",
        "description": "Returns all resources claimed by the authenticated user (or sharing a token with a claimed resource), newest first.",
        "operationId": "listMyResources",
        "security": [{"sessionCookie": []}],
        "responses": {
          "200": {
            "description": "Resource list (may be empty).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {"$ref": "#/components/schemas/Resource"}
                }
              }
            }
          },
          "401": {"description": "No valid session cookie."},
          "500": {"description": "Database error."}
        }
      }
    },
    "/api/me/resources/{token}": {
      "delete": {
        "tags": ["dashboard"],
        "summary": "Delete one of the caller's resources (paid tier only)",
        "description": "Immediately drops the underlying database (Postgres) or clears the stored requests (webhook), then marks status='deleted' in the platform DB. Only available on the paid Developer tier — free-tier resources auto-expire after 24 hours so they don't need a delete endpoint.",
        "operationId": "deleteResource",
        "security": [{"sessionCookie": []}, {"bearerAuth": []}],
        "parameters": [
          {
            "name": "token",
            "in": "path",
            "required": true,
            "schema": {"type": "string", "format": "uuid"},
            "description": "The resource token."
          }
        ],
        "responses": {
          "202": {
            "description": "Soft-deleted. The row is marked status='deleted' and the underlying database is dropped asynchronously by the reaper (typically within 5 minutes).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["ok", "id", "token", "status"],
                  "properties": {
                    "ok":      {"type": "boolean"},
                    "id":      {"type": "string", "format": "uuid"},
                    "token":   {"type": "string", "format": "uuid"},
                    "status":  {"type": "string", "example": "deleted"},
                    "message": {"type": "string"}
                  }
                }
              }
            }
          },
          "400": {"description": "Malformed token."},
          "401": {"description": "No session cookie and no valid bearer."},
          "403": {
            "description": "Caller is on the free tier. Response includes an `upgrade_url` (with the resource's token pre-attached) linking to the pricing page.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["ok", "error", "message", "upgrade_url"],
                  "properties": {
                    "ok":          {"type": "boolean", "example": false},
                    "error":       {"type": "string", "example": "paid_tier_only"},
                    "message":     {"type": "string"},
                    "upgrade_url": {"type": "string", "format": "uri"}
                  }
                }
              }
            }
          },
          "404": {"description": "Token not found or not owned by the caller."},
          "410": {"description": "Resource has already been deleted or expired."},
          "500": {"description": "Could not drop the underlying database."}
        }
      }
    },
    "/api/me/token": {
      "get": {
        "tags": ["dashboard"],
        "summary": "Mint a bearer token for CLI / agent use",
        "description": "Returns a freshly-signed JWT the caller can use as `Authorization: Bearer <token>` on /db/new, /webhook/new, and /api/me/claim. Same shape as the session cookie — any authenticated caller can mint one. 30-day TTL.",
        "operationId": "getAPIToken",
        "security": [{"sessionCookie": []}, {"bearerAuth": []}],
        "responses": {
          "200": {
            "description": "Signed JWT.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["ok", "token", "expires_in"],
                  "properties": {
                    "ok":         {"type": "boolean"},
                    "token":      {"type": "string", "description": "Paste into Authorization: Bearer header."},
                    "expires_in": {"type": "integer", "description": "Seconds until the token expires."}
                  }
                }
              }
            }
          },
          "401": {"description": "No session cookie and no valid bearer token."}
        }
      }
    },
    "/api/me/claim": {
      "post": {
        "tags": ["dashboard"],
        "summary": "Claim an existing token into the authenticated account",
        "description": "Attaches an anonymous resource (previously provisioned via /db/new or /webhook/new) to the authenticated user. Works with either a session cookie or `Authorization: Bearer <JWT>` so CLIs and agents can claim from any runtime. Idempotent when the resource already belongs to the caller.",
        "operationId": "claimToken",
        "security": [{"sessionCookie": []}, {"bearerAuth": []}],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["token"],
                "properties": {
                  "token": {"type": "string", "format": "uuid", "description": "The token returned from /db/new or /webhook/new."}
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Resource now belongs to the caller. Paid users additionally get tier='paid' and expires_at=NULL.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["ok", "id", "token", "resource_type", "tier", "status"],
                  "properties": {
                    "ok":            {"type": "boolean"},
                    "id":            {"type": "string", "format": "uuid"},
                    "token":         {"type": "string", "format": "uuid"},
                    "resource_type": {"type": "string"},
                    "name":          {"type": "string"},
                    "tier":          {"$ref": "#/components/schemas/Tier"},
                    "status":        {"type": "string"}
                  }
                }
              }
            }
          },
          "400": {"description": "Missing or malformed token."},
          "401": {"description": "No session cookie and no valid bearer token."},
          "404": {"description": "Token not found or belongs to another account."},
          "410": {"description": "Resource has already expired."},
          "500": {"description": "Database error."}
        }
      }
    },
    "/llms.txt": {
      "get": {
        "tags": ["meta"],
        "summary": "Machine-readable overview for LLM agents",
        "description": "Plain-text summary of endpoints, tiers, and usage aimed at LLMs and AI agents.",
        "operationId": "llmsTxt",
        "responses": {
          "200": {
            "description": "Plain-text overview.",
            "content": {
              "text/plain": {"schema": {"type": "string"}}
            }
          }
        }
      }
    },
    "/openapi.json": {
      "get": {
        "tags": ["meta"],
        "summary": "This OpenAPI 3.1 document",
        "description": "The machine-readable schema describing every endpoint on this server. Served as `application/json`.",
        "operationId": "openapiJSON",
        "responses": {
          "200": {
            "description": "OpenAPI 3.1 JSON document.",
            "content": {"application/json": {"schema": {"type": "object"}}}
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "sessionCookie": {
        "type": "apiKey",
        "in": "cookie",
        "name": "session",
        "description": "HttpOnly JWT set by `GET /auth/github/callback`."
      },
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "JWT",
        "description": "Same JWT as the session cookie, passed as `Authorization: Bearer <token>` for CLI / agent callers."
      }
    },
    "schemas": {
      "Tier": {
        "type": "string",
        "enum": ["anonymous", "paid"],
        "description": "Resource tier. Anonymous resources expire; paid resources do not."
      },
      "Resource": {
        "type": "object",
        "description": "A provisioned resource owned by (or claimed by) a user.",
        "required": ["id", "token", "type", "name", "tier", "status", "created_at"],
        "properties": {
          "id":         {"type": "string", "format": "uuid"},
          "token":      {"type": "string", "format": "uuid"},
          "type":       {"type": "string", "enum": ["postgres", "webhook"]},
          "name":       {"type": "string"},
          "tier":       {"$ref": "#/components/schemas/Tier"},
          "status":     {"type": "string", "enum": ["active", "expired", "deleted"]},
          "created_at": {"type": "string", "format": "date-time"},
          "expires_at": {"type": ["string", "null"], "format": "date-time", "description": "Null for paid resources."}
        }
      },
      "User": {
        "type": "object",
        "required": ["id", "github_id", "email", "created_at"],
        "properties": {
          "id":                    {"type": "string", "format": "uuid"},
          "github_id":             {"type": "integer", "format": "int64"},
          "email":                 {"type": "string", "format": "email"},
          "razorpay_customer_id":  {"type": ["string", "null"]},
          "created_at":            {"type": "string", "format": "date-time"}
        }
      },
      "PostgresLimits": {
        "type": "object",
        "properties": {
          "storage_mb":  {"type": "integer", "example": 10},
          "connections": {"type": "integer", "example": 2},
          "expires_in":  {"type": "string", "example": "24h"}
        }
      },
      "WebhookLimits": {
        "type": "object",
        "properties": {
          "requests_stored": {"type": "integer", "example": 100},
          "expires_in":      {"type": "string", "example": "24h"}
        }
      },
      "ProvisionRequest": {
        "type": "object",
        "description": "Minimal body accepted by every provisioning endpoint.",
        "required": ["name"],
        "properties": {
          "name": {"type": "string", "minLength": 1, "maxLength": 64, "example": "my-side-project", "description": "Human label; shown in the dashboard. Required."}
        }
      },
      "ProvisionResponse": {
        "type": "object",
        "description": "Postgres provisioning response.",
        "required": ["ok", "id", "token", "name", "connection_url", "tier", "limits", "note"],
        "properties": {
          "ok":             {"type": "boolean"},
          "id":             {"type": "string", "format": "uuid"},
          "token":          {"type": "string", "format": "uuid", "description": "Opaque token used to claim the resource later."},
          "name":           {"type": "string", "description": "The label supplied in the request body."},
          "connection_url": {"type": "string", "example": "postgres://user:pass@host:5432/dbname", "description": "Standard Postgres URL; usable with every driver."},
          "tier":           {"$ref": "#/components/schemas/Tier"},
          "limits":         {"$ref": "#/components/schemas/PostgresLimits"},
          "note":           {"type": "string", "description": "Human-readable hint containing the claim URL."}
        }
      },
      "WebhookProvisionResponse": {
        "type": "object",
        "description": "Webhook receiver provisioning response.",
        "required": ["ok", "id", "token", "name", "receive_url", "tier", "limits", "note"],
        "properties": {
          "ok":          {"type": "boolean"},
          "id":          {"type": "string", "format": "uuid"},
          "token":       {"type": "string", "format": "uuid"},
          "name":        {"type": "string"},
          "receive_url": {"type": "string", "format": "uri", "example": "https://api.instanode.dev/webhook/receive/9b0e..."},
          "tier":        {"$ref": "#/components/schemas/Tier"},
          "expires_at":  {"type": "string", "format": "date-time"},
          "limits":      {"$ref": "#/components/schemas/WebhookLimits"},
          "note":        {"type": "string"}
        }
      },
      "Error": {
        "type": "object",
        "required": ["ok", "error"],
        "properties": {
          "ok":      {"type": "boolean", "example": false},
          "error":   {"type": "string", "example": "provision_failed"},
          "message": {"type": "string"}
        }
      },
      "RateLimitedError": {
        "type": "object",
        "required": ["ok", "error", "message"],
        "properties": {
          "ok":      {"type": "boolean", "example": false},
          "error":   {"type": "string", "example": "rate_limited"},
          "message": {"type": "string", "example": "Daily provision limit reached (5/day). Keep resources forever: https://api.instanode.dev/start"}
        }
      }
    }
  }
}
