{
  "openapi": "3.0.3",
  "info": {
    "title": "SQLly Embedded bridge management API",
    "version": "1.0.0",
    "description": "The SQLly bridge in embedded mode exposes this API on its management listener (`--manage-bind`, default `127.0.0.1:8445`). Your web app calls it from its server side to store connection profiles, test connections, mint launch tokens for signed-in users, watch seats, and read or change the license state.\n\nEvery `/manage/...` call needs `Authorization: Bearer <manage token>` (the bridge's `--manage-token`, at least 16 characters). Bodies are JSON in and out, at most 8 MB (refused with `413 too_large` as soon as the limit is passed). Path segments and query-string values are percent-decoded; an invalid escape is `400 bad_request`. Errors are always `{ \"error\": \"<code>\", \"message\": \"<sentence>\" }`, plus `problems` when a credential document is invalid. The API sends no CORS headers: keep it on loopback or a private network and never expose it to browsers, because profiles hold database passwords.\n\nThe `Linking` operations are not on the bridge: they are sqlly.app's public endpoints an admin app uses to link an install to a license without copying a key. Their server is `https://sqlly.app`.\n\nGuide: https://sqlly.app/docs/embedded",
    "contact": { "name": "SQLly", "url": "https://sqlly.app/docs/embedded" },
    "license": { "name": "SQLly Embedded license terms", "url": "https://sqlly.app/terms" }
  },
  "externalDocs": {
    "description": "SQLly Embedded documentation",
    "url": "https://sqlly.app/docs/embedded"
  },
  "servers": [
    {
      "url": "http://{host}:{port}",
      "description": "The bridge's management listener (--manage-bind)",
      "variables": {
        "host": { "default": "127.0.0.1", "description": "Keep this on loopback or a private network." },
        "port": { "default": "8445" }
      }
    }
  ],
  "security": [ { "manageToken": [] } ],
  "tags": [
    { "name": "Health", "description": "Liveness, no token needed." },
    { "name": "License", "description": "The license the bridge runs under: state, check-in, linking and unlinking." },
    { "name": "Engines", "description": "The database engines the credential format accepts, so your UI never hard-codes the list." },
    { "name": "Profiles", "description": "A profile is a named SQLly credential document: the connections (and labels) one group of users sees. The bridge is the store of record; secrets never reach the browser." },
    { "name": "Launch", "description": "Launch tokens open SQLly for one user with one profile. Seats are concurrent users." },
    { "name": "Connections", "description": "Prove a connection works before anyone uses it." },
    { "name": "Audit", "description": "The query audit trail: every query and row edit a session ran, who ran it, against which connection, and how it ended. Kept in memory and in `<state-dir>/queries.jsonl` (SQL text included) unless the bridge runs with `--query-log false`." },
    { "name": "Linking", "description": "sqlly.app endpoints (server https://sqlly.app) for linking an install to a license: create a code, the owner approves it in their browser, collect the install's own key once, then hand it to the bridge with PUT /manage/license." }
  ],
  "paths": {
    "/healthz": {
      "get": {
        "tags": [ "Health" ],
        "operationId": "health",
        "summary": "Liveness probe",
        "description": "Answers as soon as the management listener is up. Needs no token; use it from start scripts and service health checks.",
        "security": [],
        "responses": {
          "200": {
            "description": "The management API is listening.",
            "content": { "application/json": { "schema": { "type": "object", "properties": { "ok": { "type": "boolean", "example": true } } }, "example": { "ok": true } } }
          }
        }
      }
    },
    "/manage/status": {
      "get": {
        "tags": [ "License" ],
        "operationId": "getStatus",
        "summary": "Bridge and license status",
        "description": "Everything an admin screen needs: the license state and where it came from, the allowances the signed grant carries, seats in use, open sessions, and the proxy URL browsers use.",
        "responses": {
          "200": { "description": "Current status.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/StatusReport" }, "examples": { "ok": { "$ref": "#/components/examples/StatusOk" } } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/manage/license/checkin": {
      "post": {
        "tags": [ "License" ],
        "operationId": "checkInNow",
        "summary": "Check in with sqlly.app now",
        "description": "Connected and linked licenses: runs a check-in immediately instead of waiting for the 20-minute loop. Offline licenses: re-reads the license file and never touches the network. Either way the reply is the status after the attempt; a failure leaves the state unchanged.",
        "responses": {
          "200": { "description": "Status after the check-in.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/StatusReport" } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "409": { "description": "`not_linked`: the bridge is unlicensed, there is nothing to check in.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "error": "not_linked", "message": "this bridge is not linked to a license yet" } } } },
          "422": { "description": "`license_file_invalid` (offline): the file is missing, damaged, or signed for another key.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "502": { "description": "`store_unreachable` or `grant_invalid`: sqlly.app could not be reached, or its reply did not verify.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/manage/license": {
      "put": {
        "tags": [ "License" ],
        "operationId": "linkLicense",
        "summary": "Install a linked license key",
        "description": "Gives a bridge that was started without `--license-key` / `--license-file` its key at runtime (normally the key your admin app just collected from sqlly.app through the link flow). The bridge checks in with it first and stores it (`<state-dir>/license-key`, mode 0600) only if sqlly.app answers with a verified grant; from then on it runs as a connected bridge, and the key is reused after restarts. Nothing changes on failure: the key in use keeps serving until the new one has verified. Replacing a linked key with one of another license releases the old license's instance slot after the new key verifies; a regenerated key of the same license reuses the store's instance row.",
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "$ref": "#/components/schemas/LinkLicenseRequest" }, "example": { "licenseKey": "sqe_2xH7c0VqHk9LmZr1aPq4tN8bW6yD3sJfUe5o" } } }
        },
        "responses": {
          "200": { "description": "Linked. The status now shows `license.source: linked`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/StatusReport" } } } },
          "400": { "description": "`invalid_license_key`: empty, malformed, unknown or revoked key.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "409": { "$ref": "#/components/responses/LicenseFromFlag" },
          "502": { "description": "`store_unreachable`: sqlly.app could not be reached or its reply did not verify.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      },
      "delete": {
        "tags": [ "License" ],
        "operationId": "unlinkLicense",
        "summary": "Unlink this install",
        "description": "Releases this bridge's instance slot, revokes its key on sqlly.app (best effort), deletes the stored key, closes every open session and returns the bridge to `unlicensed`.",
        "responses": {
          "204": { "description": "Unlinked." },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "description": "`not_linked`: no linked key is stored.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "409": { "$ref": "#/components/responses/LicenseFromFlag" }
        }
      }
    },
    "/manage/engines": {
      "get": {
        "tags": [ "Engines" ],
        "operationId": "listEngines",
        "summary": "Engines the credential format accepts",
        "description": "Every database engine SQLly supports, with the `engine` id to write in a credential document, its default port, whether `server` is a file path, and the `authentication.type` values it accepts.",
        "responses": {
          "200": {
            "description": "The engines.",
            "content": {
              "application/json": {
                "schema": { "type": "array", "items": { "$ref": "#/components/schemas/EngineInfo" } },
                "example": [
                  { "id": "sql-server", "name": "Microsoft SQL Server", "defaultPort": 1433, "fileBased": false, "authentication": [ "sql-login", "integrated", "entra-browser", "entra-device-code" ] },
                  { "id": "postgres", "name": "PostgreSQL", "defaultPort": 5432, "fileBased": false, "authentication": [ "sql-login" ] },
                  { "id": "sqlite", "name": "SQLite", "defaultPort": 0, "fileBased": true, "authentication": [ "none" ] }
                ]
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/manage/profiles": {
      "get": {
        "tags": [ "Profiles" ],
        "operationId": "listProfiles",
        "summary": "List profiles",
        "description": "Every stored profile without its credentials.",
        "responses": {
          "200": {
            "description": "The profiles.",
            "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/ProfileSummary" } }, "example": [ { "id": "analysts", "name": "Analysts", "connections": 2, "updatedAt": "2026-10-14T15:02:11Z" } ] } }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/manage/profiles/{id}": {
      "parameters": [ { "$ref": "#/components/parameters/ProfileId" } ],
      "get": {
        "tags": [ "Profiles" ],
        "operationId": "getProfile",
        "summary": "Read one profile",
        "description": "The profile with its full credential document, secrets included: the caller is your trusted server-side app.",
        "responses": {
          "200": { "description": "The profile.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/StoredProfile" } } } },
          "400": { "$ref": "#/components/responses/InvalidProfileId" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/UnknownProfile" }
        }
      },
      "put": {
        "tags": [ "Profiles" ],
        "operationId": "putProfile",
        "summary": "Create or replace a profile",
        "description": "Stores the profile under `id` (create or replace). The credential document is validated in full before anything is written: every problem the parser finds comes back in `problems`, and an invalid document changes nothing. A document holding a file-based engine (SQLite, DuckDB) is refused the same way unless the bridge runs with `--allow-file-engines true`, because such a connection gives its users file access on the bridge host. Sessions already open keep the catalog they started with; edits apply to the next launch.",
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PutProfileRequest" }, "examples": { "analysts": { "$ref": "#/components/examples/AnalystsProfile" } } } }
        },
        "responses": {
          "200": { "description": "Stored.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/StoredProfile" } } } },
          "400": {
            "description": "`invalid_profile` (with `problems`: parser problems, or each file-based engine while `--allow-file-engines` is off), or `invalid_profile_id`.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "error": "invalid_profile", "message": "the credential document has 2 problems", "problems": [ "connections[0].engine: unknown engine \"postgress\"", "connections[1].authentication.pasword: unknown key" ] } } }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "500": { "description": "`storage`: the state directory could not be written.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      },
      "delete": {
        "tags": [ "Profiles" ],
        "operationId": "deleteProfile",
        "summary": "Delete a profile",
        "description": "Removes the profile. Open sessions keep the catalog they started with; new launch tokens for it are refused with `unknown_profile`.",
        "responses": {
          "204": { "description": "Deleted." },
          "400": { "$ref": "#/components/responses/InvalidProfileId" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/UnknownProfile" }
        }
      }
    },
    "/manage/tokens": {
      "post": {
        "tags": [ "Launch" ],
        "operationId": "mintToken",
        "summary": "Mint a launch token",
        "description": "Creates a short-lived token that opens SQLly for `user` with `profile`. Put the returned `proxyUrl` and `token` into the page that loads SQLly (`globalThis.SQLLY_PROXY_URL` / `globalThis.SQLLY_PROXY_TOKEN`). The token can be reused until it expires (page reloads), and the profile is captured at mint time.\n\nA user holds a seat while they have an open session or an unexpired token. Minting for a user who already holds a seat never fails on the seat count; minting for a new user fails with `seat_limit` when every seat of the license is in use, counting users on every bridge of the license.",
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "$ref": "#/components/schemas/MintTokenRequest" }, "example": { "user": "dana.analyst", "profile": "analysts", "ttlSeconds": 600 } } }
        },
        "responses": {
          "201": {
            "description": "The token.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/MintedToken" }, "example": { "token": "elt_q3J0n8w2Zx5rVb1kTq9yLs4dHf7cMa6p", "expiresAt": "2026-10-14T15:12:11Z", "proxyUrl": "wss://portal.example.com/sqlly/proxy", "user": "dana.analyst", "profile": "analysts", "seatsUsed": 3, "seats": 25 } } }
          },
          "400": { "description": "`invalid_user` (empty or contains whitespace, at most 128 characters), or `invalid_profile` (the stored profile no longer validates, or holds a file-based engine while `--allow-file-engines` is off).", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/UnknownProfile" },
          "409": { "description": "`seat_limit`: every seat is in use. Release one (`DELETE /manage/seats/{user}`) or buy a larger plan.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "error": "seat_limit", "message": "all 5 seats of this license are in use; release a user or buy a larger plan" } } } },
          "503": { "description": "`license_not_ok`: the license is not `ok` or `grace` (stale, expired, revoked, not admitted, domain mismatch, or unlicensed). The message says which.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/manage/seats": {
      "get": {
        "tags": [ "Launch" ],
        "operationId": "listSeats",
        "summary": "Seats in use",
        "description": "Users holding a seat on this bridge, with their open sessions and live tokens. `seatsUsed` also counts users sqlly.app reported on other bridges of the same license.",
        "responses": {
          "200": {
            "description": "Seat usage.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SeatsReport" }, "example": { "seats": 25, "seatsUsed": 2, "users": [ { "user": "dana.analyst", "activeSessions": 1, "tokens": 1, "since": "2026-10-14T15:02:11Z" }, { "user": "sam.support", "activeSessions": 2, "tokens": 1, "since": "2026-10-14T14:40:03Z" } ] } } }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/manage/seats/{user}": {
      "parameters": [ { "name": "user", "in": "path", "required": true, "description": "The user id you minted tokens for (URL-encoded).", "schema": { "type": "string", "maxLength": 128 } } ],
      "delete": {
        "tags": [ "Launch" ],
        "operationId": "releaseSeat",
        "summary": "Release a user's seat",
        "description": "Revokes the user's tokens and closes their sessions, freeing the seat at once. Call it when a user signs out of your app or loses access. Releasing a user who holds no seat is not an error. The path segment is percent-decoded: `a%2Fb` releases the user `a/b`.",
        "responses": {
          "204": { "description": "Released." },
          "400": { "description": "`bad_request`: the segment is not valid percent-encoding.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/manage/queries": {
      "get": {
        "tags": [ "Audit" ],
        "operationId": "listQueries",
        "summary": "Query audit trail",
        "description": "Every query and row edit the bridge's sessions ran, newest first: the user and profile of the launch token, the connection it named (name, server, engine, database), the SQL text (cut to 64 KiB, see `sqlTruncated`), when it started, its status and duration, and the engine's error when it failed. Requests still running are listed with status `running`; a request refused because the license was not in order is `failed` with the license message; whatever was running when a session closed is `cancelled` with error `session closed`.\n\nThe bridge keeps the newest `--query-log-max-entries` entries (20 000 by default) in memory and in `<state-dir>/queries.jsonl`, so the trail survives a restart. With `--query-log false` nothing is recorded and the reply is `{ \"enabled\": false, \"queries\": [] }`.",
        "parameters": [
          { "name": "limit", "in": "query", "required": false, "description": "Entries to return, clamped to 1-1000.", "schema": { "type": "integer", "default": 200, "minimum": 1, "maximum": 1000 } },
          { "name": "user", "in": "query", "required": false, "description": "Only this user id (exact match, URL-encoded).", "schema": { "type": "string" } },
          { "name": "profile", "in": "query", "required": false, "description": "Only this profile id (exact match).", "schema": { "type": "string" } },
          { "name": "since", "in": "query", "required": false, "description": "Only entries whose `at` is at or after this RFC 3339 timestamp (URL-encoded).", "schema": { "type": "string", "format": "date-time" } },
          { "name": "status", "in": "query", "required": false, "description": "Only entries in this status.", "schema": { "$ref": "#/components/schemas/QueryStatus" } }
        ],
        "responses": {
          "200": {
            "description": "The matching entries, newest first.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/QueryListing" },
                "example": {
                  "enabled": true,
                  "total": 2,
                  "limit": 200,
                  "queries": [
                    { "id": "q-7", "sessionId": 12, "kind": "query", "at": "2026-10-14T15:04:09.120Z", "user": "dana.analyst", "profile": "analysts", "profileName": "Analysts", "connection": "Northwind", "server": "sql01.internal", "database": "Northwind", "engine": "Microsoft SQL Server", "sql": "SELECT TOP 100 * FROM Orders", "sqlTruncated": false, "status": "completed", "durationMs": 184, "error": null },
                    { "id": "q-6", "sessionId": 12, "kind": "query", "at": "2026-10-14T15:03:51.004Z", "user": "dana.analyst", "profile": "analysts", "profileName": "Analysts", "connection": "Northwind", "server": "sql01.internal", "database": "Northwind", "engine": "Microsoft SQL Server", "sql": "SELECT * FROM Ordres", "sqlTruncated": false, "status": "failed", "durationMs": 31, "error": "Invalid object name 'Ordres'." }
                  ]
                }
              }
            }
          },
          "400": { "description": "`bad_request`: `limit` is not a whole number, `since` is not RFC 3339, `status` is unknown, or a value is not valid percent-encoding.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/manage/test-connection": {
      "post": {
        "tags": [ "Connections" ],
        "operationId": "testConnection",
        "summary": "Test one connection",
        "description": "Runs a throw-away engine against a credential document holding exactly one connection and executes a trivial query. Connection failures are reported as `ok: false` with the engine's own message (HTTP 200); only an unusable document is an HTTP error.",
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TestConnectionRequest" }, "example": { "credentials": { "format": "sqlly-credentials", "version": 1, "connections": [ { "name": "Northwind", "engine": "sql-server", "server": "sql01.internal", "database": "Northwind", "authentication": { "type": "sql-login", "username": "reader", "password": "…" }, "tls": { "encrypt": true, "trust_server_certificate": true } } ] } } } }
        },
        "responses": {
          "200": {
            "description": "The outcome, success or not.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/TestOutcome" },
                "examples": {
                  "ok": { "summary": "Connected", "value": { "ok": true, "elapsedMs": 212, "message": "Connected: SQL Server 17.0" } },
                  "failed": { "summary": "Refused", "value": { "ok": false, "elapsedMs": 1034, "message": "Login failed for user 'reader'." } }
                }
              }
            }
          },
          "400": { "description": "`invalid_profile`: the document is invalid, does not hold exactly one connection, or holds a file-based engine while `--allow-file-engines` is off (see `problems`).", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/api/embedded/links": {
      "servers": [ { "url": "https://sqlly.app", "description": "sqlly.app" } ],
      "post": {
        "tags": [ "Linking" ],
        "operationId": "createLink",
        "summary": "Start linking an install (sqlly.app)",
        "description": "Anonymous. Creates a link request and returns a short code, a secret only your app knows, and the URL where the license owner approves it. Show the code and open `linkUrl` for the owner, then poll `POST /api/embedded/links/token` every `pollIntervalSeconds` until it is approved. Links expire after 15 minutes.",
        "security": [],
        "requestBody": {
          "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateLinkRequest" }, "example": { "name": "SQLly Embedded on portal-01", "hostname": "portal-01", "appVersion": "1.4.0" } } }
        },
        "responses": {
          "201": { "description": "Link created.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateLinkResponse" }, "example": { "linkId": "eml_Y2h9u0bQ1sXk4tWm8pZr3vLa", "code": "K7Q-4MX", "secret": "pW0r7…", "linkUrl": "https://sqlly.app/embedded/link?code=K7Q-4MX", "expiresAt": "2026-10-14T15:17:11Z", "pollIntervalSeconds": 3 } } } },
          "429": { "description": "Rate limited." },
          "503": { "description": "`link_unavailable`: no code could be allocated; try again.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/api/embedded/links/token": {
      "servers": [ { "url": "https://sqlly.app", "description": "sqlly.app" } ],
      "post": {
        "tags": [ "Linking" ],
        "operationId": "collectLink",
        "summary": "Poll for the linked key (sqlly.app)",
        "description": "Anonymous; the link secret is the credential. While the owner has not decided, answers `202 pending`. Once approved, mints this install's own license key and returns it exactly once (the link is consumed). Hand the key straight to the bridge with `PUT /manage/license`; do not store it yourself.",
        "security": [],
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "$ref": "#/components/schemas/LinkTokenRequest" } } }
        },
        "responses": {
          "200": { "description": "Approved: the key, once.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/LinkApproved" } } } },
          "202": { "description": "Still waiting for the owner.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/LinkPending" } } } },
          "401": { "description": "`wrong_secret`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "404": { "description": "`unknown_link`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "410": { "description": "`link_used`, `link_denied`, `link_expired` or `license_revoked`: this link can never complete. Start a new one.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "429": { "description": "Rate limited." }
        }
      }
    },
    "/api/embedded/public-key": {
      "servers": [ { "url": "https://sqlly.app", "description": "sqlly.app" } ],
      "get": {
        "tags": [ "Linking" ],
        "operationId": "getSigningPublicKey",
        "summary": "License-signing public key (sqlly.app)",
        "description": "The PEM public key sqlly.app signs license grants with, so an operator can compare it with the key compiled into the bridge.",
        "security": [],
        "responses": {
          "200": { "description": "PEM text.", "content": { "text/plain": { "schema": { "type": "string" } } } },
          "503": { "description": "`signing_not_configured`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "manageToken": {
        "type": "http",
        "scheme": "bearer",
        "description": "The bridge's `--manage-token` / `SQLLY_EMBEDDED_MANAGE_TOKEN` (at least 16 characters). Generate it once, e.g. `openssl rand -base64 24`, and give the same value to the bridge and your app."
      }
    },
    "parameters": {
      "ProfileId": {
        "name": "id",
        "in": "path",
        "required": true,
        "description": "Your id for the profile.",
        "schema": { "type": "string", "pattern": "^[A-Za-z0-9_-]{1,64}$", "example": "analysts" }
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "`unauthorized`: the manage token is missing or wrong.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "error": "unauthorized", "message": "management token missing or wrong" } } }
      },
      "UnknownProfile": {
        "description": "`unknown_profile`: no profile has that id.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "error": "unknown_profile", "message": "no profile with id analysts" } } }
      },
      "InvalidProfileId": {
        "description": "`invalid_profile_id`: ids are 1-64 of `A-Z a-z 0-9 _ -`.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "LicenseFromFlag": {
        "description": "`license_from_flag`: the bridge was started with `--license-key` or `--license-file`, so its license is not managed through this API.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "description": "Every error reply. `error` is a stable machine code; `message` is a sentence you can show an admin.",
        "required": [ "error", "message" ],
        "properties": {
          "error": { "type": "string", "example": "seat_limit" },
          "message": { "type": "string" },
          "problems": { "type": "array", "items": { "type": "string" }, "description": "Present for `invalid_profile`: every problem the credential parser found." }
        }
      },
      "LicenseState": {
        "type": "string",
        "description": "`ok` and `grace` serve queries; every other state refuses work with a message SQLly shows its users.",
        "enum": [ "ok", "grace", "stale", "not_admitted", "expired", "revoked", "domain_mismatch", "unlicensed" ]
      },
      "StatusReport": {
        "type": "object",
        "properties": {
          "version": { "type": "string", "description": "Bridge version.", "example": "1.1.0" },
          "instanceId": { "type": "string", "description": "This bridge process's id, as sqlly.app sees it." },
          "mode": { "type": "string", "enum": [ "connected", "offline" ] },
          "license": { "$ref": "#/components/schemas/LicenseReport" },
          "seatsUsed": { "type": "integer", "description": "Seats in use across every bridge of the license." },
          "activeSessions": { "type": "integer", "description": "Open SQLly sessions on this bridge." },
          "proxyUrl": { "type": "string", "description": "The WebSocket URL browsers use (`--public-url`).", "example": "wss://portal.example.com/sqlly/proxy" },
          "allowLocalhostOrigins": { "type": "boolean" },
          "storeUrl": { "type": "string", "example": "https://sqlly.app" },
          "allowFileEngines": { "type": "boolean", "description": "Whether profiles may hold file-based engines (`--allow-file-engines`)." }
        }
      },
      "LicenseReport": {
        "type": "object",
        "description": "Allowances come from the signed grant, never from unsigned data.",
        "properties": {
          "state": { "$ref": "#/components/schemas/LicenseState" },
          "source": { "type": "string", "enum": [ "flag", "file", "linked", "none" ], "description": "`flag`: --license-key; `file`: --license-file; `linked`: stored by PUT /manage/license; `none`: unlicensed." },
          "licenseId": { "type": "string", "nullable": true, "example": "emb_Q2x0b3VkU3FsbHkxMjM0NTY3" },
          "plan": { "type": "string", "nullable": true, "description": "Plan id, or `custom` for a license issued with its own terms.", "example": "embedded-25" },
          "planName": { "type": "string", "nullable": true, "example": "SQLly Embedded 25" },
          "status": { "type": "string", "nullable": true, "description": "sqlly.app's status from the last check-in: active, grace, expired, revoked, domain_mismatch." },
          "seats": { "type": "integer", "description": "Concurrent users the license allows." },
          "sessionsPerSeat": { "type": "integer", "description": "Open SQLly tabs one user may hold." },
          "instances": { "type": "integer", "description": "Bridges that may run at once (connected licenses; 0 offline)." },
          "domain": { "type": "string", "nullable": true, "description": "The bound domain; browsers must come from it or a subdomain.", "example": "portal.example.com" },
          "periodEnd": { "type": "string", "nullable": true, "description": "End of the paid period (ISO 8601)." },
          "graceEnd": { "type": "string", "nullable": true, "description": "Set while in grace: when the bridge stops serving." },
          "daysRemaining": { "type": "integer", "nullable": true, "description": "Whole days until the bridge stops serving." },
          "lastCheckinAt": { "type": "string", "format": "date-time", "nullable": true },
          "staleAt": { "type": "string", "format": "date-time", "nullable": true, "description": "When the bridge goes stale if no check-in succeeds (last good grant + 40 minutes)." },
          "licenseFile": { "type": "string", "nullable": true, "description": "Offline: the license file in use." },
          "message": { "type": "string", "nullable": true, "description": "The sentence users see when something is wrong (also grace countdowns)." },
          "fromCache": { "type": "boolean", "description": "True when the bridge started from its cached grant because sqlly.app was unreachable." },
          "features": { "type": "array", "items": { "type": "string" }, "example": [ "embedded", "unlimited-connections", "all-engines", "test-connection" ] }
        }
      },
      "LinkLicenseRequest": {
        "type": "object",
        "required": [ "licenseKey" ],
        "properties": { "licenseKey": { "type": "string", "description": "A key starting `sqe_`.", "maxLength": 512 } }
      },
      "EngineInfo": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "description": "The `engine` value for a credential document.", "example": "postgres" },
          "name": { "type": "string", "example": "PostgreSQL" },
          "defaultPort": { "type": "integer", "example": 5432 },
          "fileBased": { "type": "boolean", "description": "`server` is a file path and no sign-in applies." },
          "authentication": { "type": "array", "items": { "type": "string" }, "description": "Accepted `authentication.type` values." }
        }
      },
      "ProfileSummary": {
        "type": "object",
        "properties": {
          "id": { "type": "string" },
          "name": { "type": "string" },
          "connections": { "type": "integer", "description": "Number of connections in the document." },
          "updatedAt": { "type": "string", "format": "date-time" }
        }
      },
      "StoredProfile": {
        "type": "object",
        "properties": {
          "id": { "type": "string" },
          "name": { "type": "string" },
          "credentials": { "$ref": "#/components/schemas/CredentialDocument" },
          "updatedAt": { "type": "string", "format": "date-time" }
        }
      },
      "PutProfileRequest": {
        "type": "object",
        "required": [ "name", "credentials" ],
        "properties": {
          "name": { "type": "string", "description": "Display name, shown to users in SQLly." },
          "credentials": { "$ref": "#/components/schemas/CredentialDocument" }
        }
      },
      "CredentialDocument": {
        "type": "object",
        "description": "A SQLly credential document (the same format as SQLly's File → Import Connections). Unknown keys are errors, so typos are reported instead of ignored. Full reference: https://sqlly.app/docs (Credential file format).",
        "required": [ "format", "version" ],
        "additionalProperties": false,
        "properties": {
          "format": { "type": "string", "enum": [ "sqlly-credentials" ] },
          "version": { "type": "integer", "enum": [ 1 ] },
          "clients": { "type": "array", "items": { "$ref": "#/components/schemas/Label" } },
          "projects": { "type": "array", "items": { "$ref": "#/components/schemas/Label" } },
          "environments": { "type": "array", "items": { "$ref": "#/components/schemas/Label" } },
          "sql_auth_accounts": { "type": "array", "items": { "$ref": "#/components/schemas/SharedLogin" } },
          "tunnel_profiles": { "type": "array", "items": { "type": "object", "description": "`{ name, ssh?, proxy?, kube? }` - a named hop chain connections refer to with `tunnel_profile`." } },
          "connections": { "type": "array", "items": { "$ref": "#/components/schemas/Connection" } }
        }
      },
      "Label": {
        "type": "object",
        "required": [ "name" ],
        "properties": {
          "name": { "type": "string" },
          "color": { "type": "string", "description": "#RGB, #RRGGBB or #RRGGBBAA.", "example": "#1E88E5" },
          "image": { "type": "string", "description": "Icon as a data: URI or bare base64 (PNG, JPEG, GIF, WebP, BMP, ICO, SVG; up to 1 MB)." },
          "safety": {
            "type": "object",
            "description": "Only the flags you list change.",
            "properties": {
              "use_outer_transaction_rollback_scope": { "type": "boolean" },
              "only_allow_select": { "type": "boolean" },
              "use_confirmation_dialog": { "type": "boolean" },
              "require_ai_review_for_data_changes": { "type": "boolean" },
              "allow_unconstrained_update": { "type": "boolean" },
              "allow_unconstrained_delete": { "type": "boolean" },
              "require_touch_id_to_connect": { "type": "boolean" },
              "require_touch_id_for_non_select": { "type": "boolean" },
              "is_production": { "type": "boolean" }
            }
          }
        }
      },
      "SharedLogin": {
        "type": "object",
        "required": [ "name", "username" ],
        "properties": {
          "name": { "type": "string" },
          "username": { "type": "string" },
          "password": { "type": "string" }
        }
      },
      "Connection": {
        "type": "object",
        "required": [ "name", "engine", "server" ],
        "properties": {
          "name": { "type": "string" },
          "engine": { "type": "string", "description": "An id from GET /manage/engines.", "example": "postgres" },
          "server": { "type": "string", "description": "Host, IP, host\\instance, or a file path for SQLite/DuckDB." },
          "port": { "type": "integer" },
          "database": { "type": "string" },
          "environment": { "type": "string", "description": "local, dev, test, staging, production, or unset." },
          "client": { "type": "string", "description": "A client label name." },
          "project": { "type": "string", "description": "A project label name." },
          "authentication": { "$ref": "#/components/schemas/Authentication" },
          "tls": {
            "type": "object",
            "properties": {
              "mode": { "type": "string", "enum": [ "disable", "require", "verify-ca", "verify-full" ] },
              "encrypt": { "type": "boolean" },
              "trust_server_certificate": { "type": "boolean" },
              "ca_file": { "type": "string" },
              "client_cert_file": { "type": "string" },
              "client_key_file": { "type": "string" }
            }
          },
          "timeout_seconds": { "type": "integer", "default": 30 },
          "statement_timeout_seconds": { "type": "integer" },
          "application_name": { "type": "string", "default": "SQLLY" },
          "safety": {
            "type": "object",
            "properties": {
              "auto_wrap_rollback": { "type": "boolean" },
              "block_all_but_select": { "type": "boolean" },
              "confirm_data_changes": { "type": "boolean" },
              "allow_unbounded_mutations": { "type": "boolean" },
              "production_override": { "type": "boolean" }
            }
          },
          "favorite": { "type": "boolean" },
          "startup_sql": { "type": "string" },
          "tunnel_profile": { "type": "string" },
          "transport": { "type": "object", "description": "Inline proxy, ssh and kube hops." },
          "masking": { "type": "object", "description": "`{ include: [...], exclude: [...] }` column masking rules." }
        }
      },
      "Authentication": {
        "type": "object",
        "required": [ "type" ],
        "properties": {
          "type": { "type": "string", "enum": [ "sql-login", "integrated", "entra-browser", "entra-device-code", "aws-rds-iam", "gcp-cloud-sql-iam", "auth-token", "none" ] },
          "username": { "type": "string" },
          "password": { "type": "string" },
          "account": { "type": "string", "description": "Name of a shared login in sql_auth_accounts." },
          "region": { "type": "string" },
          "profile": { "type": "string" },
          "token": { "type": "string" }
        }
      },
      "MintTokenRequest": {
        "type": "object",
        "required": [ "user", "profile" ],
        "properties": {
          "user": { "type": "string", "maxLength": 128, "description": "Your stable id for the user (no whitespace). Seats are counted per distinct user." },
          "profile": { "type": "string", "description": "A profile id." },
          "ttlSeconds": { "type": "integer", "minimum": 10, "maximum": 86400, "description": "Token lifetime; defaults to `--token-ttl-seconds` (600) and is clamped to 10 s - 24 h." }
        }
      },
      "MintedToken": {
        "type": "object",
        "properties": {
          "token": { "type": "string" },
          "expiresAt": { "type": "string", "format": "date-time" },
          "proxyUrl": { "type": "string" },
          "user": { "type": "string" },
          "profile": { "type": "string" },
          "seatsUsed": { "type": "integer", "description": "Seats in use including this user." },
          "seats": { "type": "integer" }
        }
      },
      "SeatsReport": {
        "type": "object",
        "properties": {
          "seats": { "type": "integer" },
          "seatsUsed": { "type": "integer" },
          "users": { "type": "array", "items": { "$ref": "#/components/schemas/UserSeat" } }
        }
      },
      "UserSeat": {
        "type": "object",
        "properties": {
          "user": { "type": "string" },
          "activeSessions": { "type": "integer" },
          "tokens": { "type": "integer", "description": "Unexpired launch tokens." },
          "since": { "type": "string", "format": "date-time" }
        }
      },
      "TestConnectionRequest": {
        "type": "object",
        "required": [ "credentials" ],
        "properties": { "credentials": { "$ref": "#/components/schemas/CredentialDocument" } }
      },
      "TestOutcome": {
        "type": "object",
        "properties": {
          "ok": { "type": "boolean" },
          "elapsedMs": { "type": "integer" },
          "message": { "type": "string", "description": "The engine's own message, success or failure." }
        }
      },
      "QueryStatus": {
        "type": "string",
        "description": "`running` until the engine answers; then `completed`, `failed` (see `error`) or `cancelled` (by the user, or `session closed`).",
        "enum": [ "running", "completed", "failed", "cancelled" ]
      },
      "QueryEntry": {
        "type": "object",
        "description": "One audited request.",
        "properties": {
          "id": { "type": "string", "description": "The engine request id, unique within its session." },
          "sessionId": { "type": "integer", "description": "The bridge's id for the browser session." },
          "kind": { "type": "string", "enum": [ "query", "row-edit" ] },
          "at": { "type": "string", "format": "date-time", "description": "When the request reached the bridge." },
          "user": { "type": "string", "description": "The launch token's user id." },
          "profile": { "type": "string", "description": "The launch token's profile id." },
          "profileName": { "type": "string" },
          "connection": { "type": "string", "description": "The connection's name in the profile." },
          "server": { "type": "string" },
          "database": { "type": "string", "nullable": true, "description": "The profile's database when it pins one, else the database the request named." },
          "engine": { "type": "string", "description": "The engine's display name.", "example": "PostgreSQL" },
          "sql": { "type": "string", "description": "The SQL text, or `row edit: <op>` for a row edit. Cut to 64 KiB." },
          "sqlTruncated": { "type": "boolean" },
          "status": { "$ref": "#/components/schemas/QueryStatus" },
          "durationMs": { "type": "integer", "nullable": true, "description": "Null while running." },
          "error": { "type": "string", "nullable": true, "description": "The engine's error for `failed`; `session closed` for a request cut off by its session ending." }
        }
      },
      "QueryListing": {
        "type": "object",
        "properties": {
          "enabled": { "type": "boolean", "description": "False when the bridge runs with `--query-log false`; then `queries` is always empty." },
          "queries": { "type": "array", "items": { "$ref": "#/components/schemas/QueryEntry" }, "description": "Newest first." },
          "total": { "type": "integer", "description": "Entries matching the filters before `limit` was applied." },
          "limit": { "type": "integer", "description": "The page size applied (1-1000)." }
        }
      },
      "CreateLinkRequest": {
        "type": "object",
        "properties": {
          "name": { "type": "string", "maxLength": 128, "description": "Shown to the owner on the approval page and on their Embedded page." },
          "hostname": { "type": "string", "maxLength": 128 },
          "appVersion": { "type": "string", "maxLength": 32 }
        }
      },
      "CreateLinkResponse": {
        "type": "object",
        "properties": {
          "linkId": { "type": "string" },
          "code": { "type": "string", "description": "Show this to the owner so they can check it matches." },
          "secret": { "type": "string", "description": "Keep server-side; it collects the key." },
          "linkUrl": { "type": "string", "description": "Open this for the owner to approve." },
          "expiresAt": { "type": "string", "format": "date-time" },
          "pollIntervalSeconds": { "type": "integer" }
        }
      },
      "LinkTokenRequest": {
        "type": "object",
        "required": [ "linkId", "secret" ],
        "properties": { "linkId": { "type": "string" }, "secret": { "type": "string" } }
      },
      "LinkPending": {
        "type": "object",
        "properties": { "status": { "type": "string", "enum": [ "pending" ] }, "expiresAt": { "type": "string", "format": "date-time" } }
      },
      "LinkApproved": {
        "type": "object",
        "properties": {
          "status": { "type": "string", "enum": [ "approved" ] },
          "licenseKey": { "type": "string", "description": "Returned exactly once. Pass it to PUT /manage/license." },
          "keyId": { "type": "string" },
          "licenseId": { "type": "string" },
          "plan": { "type": "string", "description": "Plan id, or `custom`." },
          "planName": { "type": "string" },
          "accountEmail": { "type": "string" }
        }
      }
    },
    "examples": {
      "StatusOk": {
        "summary": "A licensed, connected bridge",
        "value": {
          "version": "1.1.0",
          "instanceId": "b5e1c0f2-7d3a-4c8e-9a61-0f2d4b7e9c13",
          "mode": "connected",
          "license": {
            "state": "ok",
            "source": "linked",
            "licenseId": "emb_Q2x0b3VkU3FsbHkxMjM0NTY3",
            "plan": "embedded-25",
            "planName": "SQLly Embedded 25",
            "status": "active",
            "seats": 25,
            "sessionsPerSeat": 4,
            "instances": 1,
            "domain": "portal.example.com",
            "periodEnd": "2027-10-14T00:00:00+00:00",
            "graceEnd": null,
            "daysRemaining": 365,
            "lastCheckinAt": "2026-10-14T15:00:02Z",
            "staleAt": "2026-10-14T15:40:02Z",
            "licenseFile": null,
            "message": null,
            "fromCache": false,
            "features": [ "embedded", "unlimited-connections", "all-engines", "test-connection" ]
          },
          "seatsUsed": 3,
          "activeSessions": 4,
          "proxyUrl": "wss://portal.example.com/sqlly/proxy",
          "allowLocalhostOrigins": false,
          "storeUrl": "https://sqlly.app",
          "allowFileEngines": false
        }
      },
      "AnalystsProfile": {
        "summary": "A profile with a shared login and labels",
        "value": {
          "name": "Analysts",
          "credentials": {
            "format": "sqlly-credentials",
            "version": 1,
            "clients": [ { "name": "Northwind Traders", "color": "#1E88E5" } ],
            "environments": [ { "name": "Production", "color": "#C62828", "safety": { "only_allow_select": true } } ],
            "sql_auth_accounts": [ { "name": "Reader", "username": "reader", "password": "…" } ],
            "connections": [
              { "name": "Northwind", "engine": "sql-server", "server": "sql01.internal", "database": "Northwind", "environment": "production", "client": "Northwind Traders", "authentication": { "type": "sql-login", "account": "Reader" }, "tls": { "encrypt": true, "trust_server_certificate": true } },
              { "name": "Warehouse", "engine": "postgres", "server": "dw.internal", "database": "dw", "environment": "production", "client": "Northwind Traders", "authentication": { "type": "sql-login", "account": "Reader" }, "tls": { "mode": "verify-full" } }
            ]
          }
        }
      }
    }
  }
}
