API Surface Design#

Detailed API surface for the scafctl REST API server. All endpoints use JSON request/response bodies and follow REST conventions.

Base URL#

http://localhost:8080

Version-prefixed endpoints use /{apiVersion}/ (default: /v1/).

Authentication#

  • Method: Entra OIDC (Azure AD) JWT Bearer tokens
  • Header: Authorization: Bearer <token>
  • Public endpoints: Health probes and metrics bypass authentication
  • Admin endpoints: Require authentication + admin role claim (or localhost-only when auth is disabled)

Response Format#

Success (single resource)#

{
  "fieldA": "value",
  "fieldB": 42
}

Success (collection with pagination)#

{
  "items": [...],
  "pagination": {
    "page": 1,
    "per_page": 100,
    "total_items": 250,
    "total_pages": 3,
    "has_more": true
  }
}

Error (RFC 7807 Problem Details)#

{
  "title": "Bad Request",
  "status": 400,
  "detail": "validation failed: name is required"
}

Naming Conventions#

LayerConventionExample
Config (YAML/JSON)camelCaseapiVersion, shutdownTimeout
API response fieldssnake_caseper_page, total_items, has_more
URL pathslowercase, kebab-case for multi-word/v1/solutions, /v1/admin/reload-config
Query parameterssnake_caseper_page, filter

Endpoints#

Operational (Root Router — No Auth)#

MethodPathDescriptionStatus Codes
GET/API root with HATEOAS links200
GET/healthFull health check with component status200
GET/health/liveLiveness probe200
GET/health/readyReadiness probe200, 503
GET/metricsPrometheus metrics200

Solutions#

MethodPathDescriptionQuery ParamsStatus Codes
POST/v1/solutions/runRun a solution (resolve + execute actions)—200, 400, 422
POST/v1/solutions/renderResolve inputs without executing actions—200, 400, 422
POST/v1/solutions/lintLint a solution—200, 400
POST/v1/solutions/inspectInspect solution structure—200, 400
POST/v1/solutions/testValidate solution against provider registry—200, 400
POST/v1/solutions/dryrunDry-run a solution (what-if analysis)—200, 400, 422

Providers#

MethodPathDescriptionQuery ParamsStatus Codes
GET/v1/providersList providerspage, per_page, filter200, 400
GET/v1/providers/{name}Get provider details—200, 404
GET/v1/providers/{name}/schemaGet provider schema—200, 404

Eval#

MethodPathDescriptionStatus Codes
POST/v1/eval/celEvaluate CEL expression200, 400
POST/v1/eval/templateEvaluate Go template200, 400

Catalogs#

MethodPathDescriptionQuery ParamsStatus Codes
GET/v1/catalogsList catalogspage, per_page, filter200, 400
GET/v1/catalogs/{name}Get catalog details—200, 404
GET/v1/catalogs/{name}/solutionsList catalog solutionspage, per_page200, 404
POST/v1/catalogs/syncSync catalogs (planned)—200, 400

Schemas#

MethodPathDescriptionQuery ParamsStatus Codes
GET/v1/schemasList schemaspage, per_page200
GET/v1/schemas/{name}Get schema—200, 404
POST/v1/schemas/validateValidate against schema—200, 400, 422

Config#

MethodPathDescriptionStatus Codes
GET/v1/configGet current config200
GET/v1/settingsGet runtime settings200

Snapshots#

MethodPathDescriptionQuery ParamsStatus Codes
GET/v1/snapshotsList snapshots (planned)page, per_page200
GET/v1/snapshots/{id}Get snapshot details (planned)—200, 404

Explain & Diff#

MethodPathDescriptionStatus Codes
POST/v1/explainExplain a solution200, 400
POST/v1/diffDiff solutions200, 400

Admin#

MethodPathDescriptionAuthorizationStatus Codes
GET/v1/admin/infoServer infoadmin role / localhost200, 403
POST/v1/admin/reload-configHot-reload config (planned)admin role / localhost200, 403
POST/v1/admin/clear-cacheClear caches (planned)admin role / localhost200, 403

Documentation (Auto-Generated by Huma)#

MethodPathDescriptionStatus Codes
GET/v1/docsInteractive API documentation200
GET/v1/openapi.jsonOpenAPI specification (JSON)200

Common Query Parameters#

ParameterTypeDescriptionDefaultConstraints
pageintPage number (1-indexed)11–10000
per_pageintItems per page1001–1000
filterstringCEL filter expression—max 2000 chars

Global Status Codes#

These status codes can be returned by any endpoint, in addition to endpoint-specific codes:

CodeCondition
401 UnauthorizedMissing or invalid auth token (when auth is enabled)
403 ForbiddenInsufficient role (admin endpoints), or non-loopback with auth disabled
405 Method Not AllowedHTTP method not supported for this resource
413 Request Entity Too LargeRequest body exceeds maxRequestSize (default 10 MB)
429 Too Many RequestsRate limit exceeded (includes Retry-After header)
503 Service UnavailableServer is shutting down (readiness probe only)

Middleware Stack#

The API uses a two-layer middleware architecture. Health probes and metrics bypass the API middleware entirely.

Global Middleware (All Routes)#

OrderMiddlewarePurpose
1Panic RecoveryCatches panics, logs stack trace, returns 500
2Request IDGenerates unique X-Request-ID per request
3Flight IDCorrelation ID spanning the full request lifecycle
4Strip SlashesNormalizes trailing slashes in URLs
5Request LoggingStructured log of method, path, status, duration
6Token PassthroughForwards allow-listed caller tokens to providers (when configured)

API Middleware (Business Endpoints)#

OrderMiddlewarePurpose
0Host AllowlistRejects unexpected Host headers (DNS-rebinding protection); no-op when allowedHosts is empty
1CORSCross-Origin Resource Sharing (configurable)
2TimeoutRequest timeout enforcement
3ThrottleMax concurrent in-flight requests
4AuthenticationEntra OIDC JWT validation
4bAdmin Authorization/v1/admin/* only: requires the admin role, or a loopback peer when auth is disabled
5Rate LimitingPer-IP sliding window limiter
6Max Body SizeRequest body size validation
7Compressiongzip response compression
8Security HeadersCSP, X-Frame-Options, HSTS, etc.
9MetricsOpenTelemetry metrics collection
10Audit LoggingStructured audit logs with field redaction
11TracingOpenTelemetry distributed tracing

Rate Limiting#

When rate limiting is enabled, every API response includes standard rate limit headers:

HeaderDescription
X-RateLimit-LimitMaximum requests allowed in the window
X-RateLimit-RemainingRequests remaining in the current window
X-RateLimit-ResetUnix timestamp when the rate limit window resets
Retry-AfterDuration until retry is allowed (only on 429 responses)

Security#

Security Headers#

All API responses include the following security headers:

HeaderValue
X-Content-Type-Optionsnosniff
X-Frame-OptionsDENY
Content-Security-Policydefault-src 'none'
X-XSS-Protection0
Referrer-Policystrict-origin-when-cross-origin
Permissions-Policycamera=(), microphone=(), geolocation=()
Strict-Transport-Securitymax-age=63072000; includeSubDomains; preload (TLS only)

TLS Support#

The server supports TLS with configurable certificate and key paths. When TLS is enabled, HSTS headers are automatically added to all responses.

CEL Filter Sandboxing#

User-supplied filter expressions are evaluated with:

  • Cost limit: 10,000 (lower than CLI default of 1,000,000)
  • Read-only: No I/O, no mutations, no filesystem access
  • Max length: 2,000 characters

Audit Log Redaction#

Request bodies in audit logs have sensitive fields redacted:

  • Fields matching password, secret, token, key, credential, authorization → [REDACTED]

Admin Authorization#

Auth StateAdmin Access
Entra OIDC enabledRequires admin role in JWT claims
Auth disabledLocalhost-only (non-loopback -> 403)

The loopback check reads only the peer address (RemoteAddr). Proxy headers (X-Forwarded-For, Forwarded, X-Real-IP) are never trusted to grant access – a remote caller cannot claim to be localhost by setting them.

Reverse-proxy caveat: when the server is fronted by a proxy on the same host, every forwarded request arrives with RemoteAddr = 127.0.0.1, which would otherwise satisfy the loopback check for internet callers. To close that, a loopback request carrying any proxy-hop header is denied rather than allowed, since those headers prove a hop occurred. Operators fronting the server with a proxy should additionally block /v1/admin/ at the proxy and enable authentication.


Server Configuration#

The API server is configured via the apiServer section in the scafctl config file:

FieldTypeDefaultDescription
hoststring127.0.0.1Bind address. Binding a non-loopback address with auth disabled logs a startup warning.
portint8080Listen port
apiVersionstringv1URL path version prefix
requestTimeoutstring60sPer-request timeout (also used for ReadTimeout/WriteTimeout)
idleTimeoutstring60sKeep-alive idle connection timeout. When unset, Go falls back to ReadTimeout, so the default matches requestTimeout rather than extending it.
maxHeaderBytesint1048576Max size of request headers in bytes. Capped at 4 MB; a larger value is rejected at startup by config validation.
shutdownTimeoutstring30sGraceful shutdown window
maxConcurrentint1000Max concurrent in-flight requests
maxRequestSizeint6410485760Max request body size (bytes)
allowedHosts[]string–Permitted Host header values (DNS-rebinding protection). Empty means every Host is accepted. Supports *.example.com wildcards (subdomains at any depth, but not the bare apex – list it separately). A bare * is the explicit accept-everything opt-out; a non-empty list whose entries are all blank or malformed is rejected at startup rather than silently disabling the check. Capped at 50 entries, enforced at startup. Applies to /v1/* only; /health and /metrics are exempt so k8s probes keep working.
compression.levelint6Gzip compression level
cors.enabledboolfalseEnable CORS
cors.allowedOrigins[]string—Allowed origins
cors.allowedMethods[]string—Allowed HTTP methods
cors.allowedHeaders[]string—Allowed headers
cors.maxAgeint—Preflight cache duration (seconds)
tls.enabledboolfalseEnable TLS
tls.certstring—Path to TLS certificate
tls.keystring—Path to TLS private key
auth.azureOIDC.enabledboolfalseEnable Entra OIDC auth
auth.azureOIDC.tenantIdstring—Azure AD tenant ID
auth.azureOIDC.clientIdstring—Azure AD client ID
rateLimit.global.maxRequestsint100Max requests per window. Applied by default – unset does not mean disabled, and 0 denies every request, so running unlimited means setting a very high value
rateLimit.global.windowstring1mRate limit window duration
rateLimit.global.trustProxyboolfalseKey the limiter on X-Forwarded-For/X-Real-IP instead of RemoteAddr. Only enable behind a proxy that sanitizes those headers, or clients can spoof their IP to bypass the limit
audit.enabledboolfalseEnable audit logging
tracing.enabledboolfalseEnable OpenTelemetry tracing

Where these bounds are enforced#

The maximum / maxItems struct tags on APIServerConfig describe the bounds to schema consumers, but the config loader does not apply struct tags. The values above are therefore checked by APIServerConfig.Validate, which runs on every path that can start a server:

  • config.Manager.Load – the file/env/flag configuration path used by scafctl serve.
  • api.NewServer – the embedder path. NewServer(WithServerConfig(cfg)) accepts a hand-built config that never went through the loader, so it validates before constructing anything and returns an error on a violation.
  • api.SetupMiddleware – exported separately, takes its own *config.APIServerConfig, and is the only consumer of allowedHosts.

Embedder note: NewServer and SetupMiddleware return an error for an apiServer config that exceeds a documented bound. A limit that only the file-loading path honours is not a limit, so this is deliberate – but an embedder that was previously passing an out-of-bounds value will now fail at construction instead of silently running unbounded.