FastSSV HTTP API¶
The fastssv.api subpackage exposes the validator as a FastAPI service. It is
optional — the core library and CLI work without it. Install with the api
extra:
This pulls in fastapi, uvicorn[standard], gunicorn, slowapi, and
pydantic-settings.
Running¶
There are two supported "one command" paths. Both launch the JSON API and the HTMX web UI from the same process (they're mounted on the same app).
uv run fastssv serve — host Python¶
Works for local dev, demos, and single-VM production. No Docker required.
uv run fastssv serve # dev: uvicorn, host 127.0.0.1, port 8000
uv run fastssv serve --reload # + auto-reload on code changes
uv run fastssv serve --host 0.0.0.0 --port 9000
uv run fastssv serve --prod # gunicorn + 2 uvicorn workers
uv run fastssv serve --prod --workers 4 # tune worker count
Under the hood: dev mode invokes uvicorn.run(...) in-process; --prod execs
gunicorn -k uvicorn.workers.UvicornWorker .... Each worker loads the full
rule registry once at startup (~154 rules, sub-second).
The same process also exposes an MCP Streamable HTTP endpoint at /mcp when the optional [mcp] extra is installed (uv add "fastssv[api,mcp]"). See MCP server for tool surface, auth posture, and client setup.
docker compose up — containerized¶
Use this for servers, CI, or when you want container isolation to match
production. The compose file wraps the existing deploy/Dockerfile.
docker compose -f deploy/docker-compose.yml up --build
docker compose -f deploy/docker-compose.yml down
Environment variables set in deploy/docker-compose.yml override the defaults
(log level, rate limit, body-size cap, parse timeout, CORS origins, worker
count). See the comments in that file or the Configuration section below.
The container runs the same gunicorn command as uv run fastssv serve --prod,
uses a non-root user, mounts a read-only root filesystem, and ships a
healthcheck against /v1/health.
Configuration¶
All configuration is via environment variables with the FASTSSV_API_
prefix. Defaults are production-sane.
| Variable | Default | Description |
|---|---|---|
FASTSSV_API_MAX_SQL_BYTES |
100000 |
Maximum SQL body size. Requests exceeding this return 413. |
FASTSSV_API_PARSE_TIMEOUT_SECONDS |
5.0 |
Hard ceiling per validation call. Exceeded → 408. |
FASTSSV_API_RATE_LIMIT |
60/minute |
slowapi-format limit applied per client IP. |
FASTSSV_API_CORS_ORIGINS |
[] |
Comma-separated list of allowed origins. Empty = CORS disabled. |
FASTSSV_API_LOG_LEVEL |
INFO |
Root logger level (DEBUG/INFO/WARNING/ERROR). |
FASTSSV_API_BEHIND_PROXY |
false |
Trust X-Forwarded-* headers from a reverse proxy. The compose file defaults this to true; in-code default is false for direct execution. |
FASTSSV_API_MCP_ENABLED |
false |
Mount the MCP Streamable HTTP endpoint at /mcp (requires the [mcp] extra). Opt-in because the endpoint is unauthenticated at the app layer; gate it at your reverse proxy before enabling. See MCP server. |
FASTSSV_API_MCP_ALLOWED_ORIGINS |
[] |
CSV/JSON list of Origin values permitted on /mcp from a browser. Empty = closed (any present-but-unlisted Origin → 403). |
FASTSSV_API_MCP_AUTH_MODE |
none |
Reserved for future OAuth 2.1 conformance. Today pinned to "none". |
A .env file in the working directory is loaded automatically.
Web UI¶
In addition to the JSON API, the service ships with a minimal HTMX-based web interface for ad-hoc validation:
GET /— paste SQL, pick a dialect, submit; violations render inline via HTMX fragment swap. No JS framework, no build step.GET /rules— browsable list of every registered rule with a client-side filter by id/name/description and category.GET /static/*— vendored HTMX (htmx.min.js) andstyle.css.
UI form submissions go through POST /ui/validate, which returns an HTML
fragment (not JSON). It shares the same middleware stack as the JSON API —
body-size limit, parse timeout, rate limiting, security headers.
Endpoints¶
All JSON endpoints are versioned under /v1. Error responses use a uniform
schema (error, message, request_id).
POST /v1/validate¶
Validate a single SQL query.
Request:
| Field | Type | Required | Notes |
|---|---|---|---|
sql |
string | yes | Non-empty. Subject to MAX_SQL_BYTES. |
dialect |
enum | no | Default "auto". One of: "auto", "postgres", "tsql", "oracle", "redshift", "bigquery", "snowflake", "databricks", "duckdb". |
strict |
boolean | no | Default false. When true, best-practice warnings escalate to errors. Same semantics as the CLI --strict flag. |
Response (200):
{
"is_valid": false,
"error_count": 1,
"warning_count": 0,
"errors": [
{
"rule_id": "data_quality.schema_validation",
"severity": "error",
"issue": "Table 'no_such_table' does not exist in OMOP CDM 5.4 schema.",
"fix": "Ensure all table and column names match the OMOP CDM 5.4 schema",
"location": null
}
],
"warnings": [],
"query_count": 1,
"results": [
{
"query_index": 1,
"sql": "SELECT * FROM no_such_table;",
"is_valid": false,
"error_count": 1,
"warning_count": 0,
"errors": [{"rule_id": "data_quality.schema_validation", "issue": "…", "..." : "..."}],
"warnings": []
}
],
"dialect": "auto",
"duration_ms": 8.7,
"strict": false
}
The Violation shape is { rule_id, severity, issue, fix, location }. fix is heterogeneous: a prose string for free-form patches, or a structured patch object ({"action": "REPLACE"|"ADD"|"REMOVE", "span": [s,e]|"at": pos, "text": ...}) for mechanical ones — clients should switch on typeof fix === "string" vs object. The CLI JSON report uses the same fix field naming; see JSON output for the CLI report shape.
Note on dialect in the response. The service echoes req.dialect as-is — if you submit "dialect": "auto", the response also reads "dialect": "auto", not the dialect that auto-detection resolved to. To learn which dialect was actually used for parsing, call fastssv.core.helpers.detect_dialect(sql) client-side or pass an explicit dialect on the request.
Multi-statement input. If sql contains multiple ;-separated
statements, the service splits them (comment- and quote-aware) and
validates each independently. Top-level fields are cross-statement
aggregates; results[i] attributes each error/warning to its source
query via query_index (1-based). Single-statement submissions still
return a one-element results list.
Error status codes:
- 400 — malformed body
- 408 — validation exceeded PARSE_TIMEOUT_SECONDS
- 413 — body larger than MAX_SQL_BYTES
- 422 — request failed schema validation (missing SQL, bad dialect enum)
- 429 — rate limit exceeded
GET /v1/rules¶
List every registered rule. Useful for a frontend that wants to render rule metadata, filter by category, etc.
Response (200):
{
"total": 154,
"rules": [
{
"rule_id": "anti_patterns.ambiguous_column_reference",
"name": "Ambiguous Column Reference",
"description": "Detects unqualified column references ...",
"severity": "warning",
"category": "anti_patterns"
}
]
}
GET /v1/health¶
Liveness probe. Always 200 OK unless the process cannot service requests.
Response (200):
Production guardrails¶
The service is designed to be exposed to untrusted clients.
- Body-size enforcement runs as ASGI middleware: oversized requests are
rejected by
Content-Lengthbefore the body is read. - Parse-timeout uses
asyncio.wait_foraroundasyncio.to_thread, so the CPU-bound validator runs off the event loop and cannot wedge the server. - Rate limiting via
slowapi, keyed by client IP. Stored in memory by default (fine for single-instance deployments). Swap for Redis when you scale horizontally. - CORS is strict — an explicit whitelist is required to enable it. No wildcards.
- Security headers set on every response:
X-Content-Type-Options,X-Frame-Options,Referrer-Policy,Strict-Transport-Security,Permissions-Policy. - Request IDs are generated per request and returned in
x-request-id. Clients can override by sending the same header. - Structured JSON logging reuses the core
JSONFormatter. Every validation logssql_hash,dialect, counts, and timing — never the SQL body, to avoid accidentally persisting sensitive data. - Exception handlers return the uniform
ErrorResponseschema. Stack traces are logged but never sent to the client. - Versioned routes (
/v1/...) so the API can evolve without breaking deployed clients.
Deployment notes¶
- Reverse proxy: put the service behind nginx / Cloudflare / a cloud load
balancer that terminates TLS. The
Strict-Transport-Securityheader the service sets assumes HTTPS is handled upstream. - Scaling: the service is stateless, so horizontal scaling is trivial.
When you run more than one instance, switch
slowapistorage to Redis so rate limits are shared across workers. - Observability: the JSON log lines are designed to be shipped as-is to a
log aggregator (Datadog, Grafana Loki, CloudWatch). Key fields:
sql_hash(never the SQL itself),dialect,errors,warnings,duration_ms,client,request_id. - Health checks: point your orchestrator at
/v1/health. The Dockerfile already does this viaHEALTHCHECK.
Extending¶
- Auth: the MVP has none — the service is intended to run behind an API
gateway that handles authentication. If you want in-process auth, add a
FastAPI dependency on every route in
routes.py. - Metrics: Prometheus is intentionally not bundled. Add
prometheus-clientand a/metricsroute if your platform doesn't already scrape request metrics. - Redis rate limiting: pass a
storage_uritoLimiter(...)infastssv/api/app.pyto swap the in-memory backend for Redis when you scale out.