
Public APIs run inside a single trust boundary. Everything inbound is hostile
until it has passed a generated decoder, a domain validator, an encoder, or
all three. This page is the reference for the pieces involved and the
compile-time gates that enforce them.

## Trust boundary

Every public request enters Vary through the generated HTTP boundary
(`expose Service via http` or a `service` inside an `api` block). The boundary
is the only place that produces tainted values; everything else operates on
validated domain types.

```mermaid
flowchart TD
    REQ[Public request] --> BOUNDARY[Generated HTTP boundary]
    BOUNDARY -- "Tainted[Str], Tainted[Int], ..." --> VALIDATE[Domain validators]
    VALIDATE -- "IssueTitle, MarkdownBody, PublicUrl, ..." --> LOGIC[Service code]
    LOGIC --> ENVELOPE[api_response envelopes]
    ENVELOPE --> RESP[Response]
    LOGIC -. blocked .- SINK[Unsafe sinks]
    VALIDATE --> SINK
```

The compiler treats `Tainted[T]` and known-unsafe sinks as opposites. A
`Tainted[Str]` cannot flow into raw SQL, raw shell, raw HTML, or any other
sink without first being validated or encoded. The check is structural - it
runs in `TaintTypeCheckerTest` and `ContentTypeCheckerTest` before code
generation, so a broken route fails to compile.

## Tainted values

`Tainted[T]` is the type of any value that came from a public request. The
generated decoder hands the service tainted values for every body, query, and
header field that is not already covered by a stronger validator. A tainted
value carries the original `T` but cannot be passed to anything that would
treat it as trusted - assigning it to a `Str` parameter, indexing into a
collection by it, or passing it to a sink function are all compile errors.

```vary
interface IssueApi {
    def create_issue(self, title: Tainted[Str], body: Tainted[Str]) -> Str { }
}
```

The handler removes the taint by calling one of a small set of validator
methods. Each returns `Result[T, ValidationError]`, so the type system forces
the failure case to be handled. Combined with the `?` propagation operator,
validation reads as a straight line:

```text
let title_str  = title.validate_str(1, 160)?
let valid_title = IssueTitle.validate(title_str)?
```

| Method | Signature | Purpose |
|---|---|---|
| `.validate_str(min, max)` | `(Int, Int) -> Result[Str, ValidationError]` | Length-checked unwrap to `Str`. |
| `.validate_int(min, max)` | `(Int, Int) -> Result[Int, ValidationError]` | Range-checked unwrap to `Int`. |
| `.validate(<DomainType>)` | `(Type) -> Result[T, ValidationError]` | Apply a domain validator. |
| `.raw()` | `() -> Str` | Escape hatch for explicit tests only; flagged in production code. |

There is no implicit conversion. If a route has not validated a field, that
field cannot reach a database call, a markdown renderer, or a log line - the
compiler stops the build at the call site, not at runtime.

## Validators

There are three ways to declare a validator. Use the first that fits, in this
order:

### 1. Inline shape metadata

For lengths, ranges, enums, and named field validators, use the metadata
clauses on `shape` fields. The compiler enforces them on the generated
decoder.

```text
shape CreateIssueRequest {
    required title, body
    title: Str length 1..200
    severity: Str enum ["low", "medium", "high"]
    body: Str length 1..20000
    url: Str length 8..2048 validate PublicWebhookUrl
}
```

Available per-field metadata:

| Clause | Purpose |
|---|---|
| `length min..max` | Inclusive length bounds for `Str` fields. |
| `range min..max` | Inclusive numeric bounds for `Int` or `Float` fields. |
| `enum [...]` / `enum QualifiedName` | Inline literal set or reference to a declared `enum`. |
| `validate <name>` | Apply a named validator (e.g. `validate PublicWebhookUrl`). |
| `default <literal>` | Default applied when the request omits the field. |
| `format <name>` | Format hint exported to OpenAPI (e.g. `email`, `uri`, `uuid`). |
| `description "..."` | Free-text description carried into OpenAPI and the generated client. |
| `deprecated "..." [replacement "..."]` | Marks the field as deprecated; OpenAPI carries the migration hint. |

Shape-level constraints sit on the line above the fields. `required <names>`
forces those fields to be present; `at_least_one <names>` requires the
request to carry at least one of the listed fields.

### 2. Named validators

For reusable policies (e.g. SSRF-safe outbound URLs), declare a `validator`
inside the `api` block and reference it by name:

```vary
api IssueTrackerApi version "v1" base "/api/v1" stability stable {
    validator PublicWebhookUrl = UrlPolicy {
        schemes ["https"]
        host public
        dns public
        redirects public
        deny_internal_services
    }

    shape RegisterWebhookRequest {
        required url
        url: Str length 8..2048 validate PublicWebhookUrl
    }
}
```

`UrlPolicy` is the only built-in validator family today. It encodes scheme,
host, DNS, and redirect rules and is backed by `stdlib/url_policy.vary`
(see [stdlib](/docs/stdlib/)).

### 3. Domain types

For values that should survive past the request boundary, define a domain
type with `validate(raw)` and use it everywhere downstream. The validator
returns a `Result`, so the type system forces handling:

```text
let title_str  = title.validate_str(1, 160)?
let valid_title = IssueTitle.validate(title_str)?
let valid_body  = MarkdownBody.validate(body.validate_str(0, 65536)?)?
```

Issue tracker examples: `IssueTitle`, `MarkdownBody`, `Username`,
`SearchText`, `PublicUrl`. Each is a `data` type whose constructor is private;
the public entry point is `Type.validate(raw)`.

## Unsafe sinks

Once a value is validated or encoded it can reach a sink. Direct flow from
`Tainted[T]` into any of the following is a compile error:

| Unsafe sink | Required validator or encoder |
|---|---|
| SQL query values | Typed SQL DSL values, prepared parameters, or validated domain IDs. |
| SQL schema and DDL strings | Static migration text reviewed with `Sql.schema(...)` and migration tests. |
| Shell/process arguments | Typed process capability plus allow-listed command and validated arguments. |
| Template rendering | Plain text escaping for text slots; `SafeHtml` only for trusted HTML slots. |
| Markdown rendering | `MarkdownBody.validate(raw)` then `Markdown.render_body(...).safe_html()`. |
| HTML response body | `SafeHtml`; never raw `Str` from a request. |
| XML/Atom response body | `import xml` helpers; never string-concatenated markup. |
| Logs and audit details | Structured log fields with secret redaction and line-length limits. |
| URL parsing/building | `PublicUrl.validate(raw)` for public URLs; route builders for internal paths. |
| Outbound HTTP/service clients | Typed service declaration plus host authority allow-list. |
| Search indexing | `SearchText.validate(raw)` or `SearchIndex.normalize(SearchText)`. |
| Config keys | Declared config key name and Via config policy; no public request-controlled key lookup. |
| Add-on binding names | Declared binding such as `DATABASE`; no user-controlled binding name. |
| File paths | Typed filesystem capability/root types; no public request-controlled absolute path. |

The type checker rejects direct `Tainted[...]` flow into known unsafe runtime
calls. New sink APIs must register with the taint safety checker so the
compile-fail coverage continues to protect public APIs.

## Auth, capabilities, and idempotency

Endpoint clauses encode the auth and abuse-control story directly in the
route contract. The compiler exports them to OpenAPI, the route manifest, and
the generated client.

```text
service IssueTrackerService at "https://issue-tracker.local" {
    get list_issues "/issues" query ListIssuesQuery -> IssueListResponse
        auth none
        cache public(max_age=60, s_maxage=300)
        capability issues.private.read

    post create_issue "/issues" body CreateIssueRequest -> IssueDetailResponse
        auth session
        idempotent
        cache private_no_store
        capabilities issues.create, issues.write
}
```

| Clause | Meaning |
|---|---|
| `auth none` | Unauthenticated; access is governed entirely by capabilities and rate limits. |
| `auth optional` | Authentication is parsed when present but not required; the handler reads `RequestContext.auth_verified` to branch. |
| `auth session` | Requires a valid session token. |
| `auth capability <name>` | Requires both a session and a specific capability (e.g. `auth capability issues.moderate`). |
| `capability <name>` / `capabilities <a>, <b>` | Capabilities the handler must hold; checked by the authorizer before the handler runs. |
| `cache public(max_age=N, s_maxage=N)` | Public caching with the declared TTLs. |
| `cache private_no_store` | No caching layer may store the response. |
| `idempotent` | The handler accepts an `Idempotency-Key` header; replays are deduplicated. |
| `paginate cursor <field>` | Marks the response as cursor-paginated; tools wire the named field to the next-cursor header. |
| `cli command "..."` | Projects the route into the generated `vary api cli` command surface. |
| `fields public <policy>` | Limits the response to fields visible to the named projection policy. |

`auth` and `capability` look similar but answer different questions. `auth`
governs *who* the caller is (anonymous, optional session, a logged-in
session, a logged-in session that also holds capability X). `capability`
governs *what the route needs* and applies even on anonymous routes - it is
the input the authorizer uses when matching the caller's capability set
against the route. A common shape is `auth none` plus a public-read
capability like `issues.private.read` for unauthenticated reads of issues
the caller can see, alongside `auth session` plus a mutation capability
like `issues.write` for changes.

Route auth is enforced by the generated Javalin boundary metadata before
the Vary handler runs. The compiler emits framework-independent route
metadata and Javalin bindings; application code does not write JAX-RS, CDI,
or other framework annotations.

`idempotent` is more than a marker. It tells the boundary to look for an
`Idempotency-Key` header, dedupe against the audit log for that key, and
return the original response on replay. Mutations without `idempotent` are
treated as fire-and-forget by clients and retried freely - which is the wrong
default for anything that writes state.

Capabilities are declared in code with `stdlib/access.vary`:

```text
import access

let cap_read     = access.capability("issues.private.read")
let cap_moderate = access.capability("issues.moderate")
let rate_key     = access.rate_limit_key(ip, user_id, route, action, trust_level)
```

The capability matrix is asserted by `PolicyGateCapabilityTest`; rate limit
keys are exercised by `VaryAccessControlTest`.

## Production auth configuration

`auth optional`, `auth session`, and `auth capability` routes require a
configured authentication mode at deploy time. The Vary HTTP runtime owns
session validation, CSRF, and API token verification - apps do not write
their own auth middleware. The runtime reads its configuration from the
following environment variables, supplied by Via at launch:

| Variable | Purpose |
|---|---|
| `VARY_AUTH_MODE` | Selects the authentication source. Accepted values: `oidc` for Authorization Code + PKCE through an OIDC provider, `jwt` for verified bearer JWTs. Any other value is rejected at startup. |
| `VARY_AUTH_OIDC_ISSUER` | Issuer URL discovered for the OIDC provider. |
| `VARY_AUTH_OIDC_CLIENT_ID` | Client identifier registered with the provider. |
| `VARY_AUTH_OIDC_SCOPES` | Requested scopes (default `openid profile email`). |
| `VARY_AUTH_CAPABILITY_CLAIM` | JWT/OIDC claim path that supplies the caller's capability list. |
| `VARY_AUTH_CAPABILITY_PREFIX` | Optional prefix stripped from each capability claim value. |
| `VARY_AUTH_CSRF_MODE` | CSRF mode: `strict` (default for session writes), `same_site_lax`, or `disabled` for read-only deployments. |
| `SESSION_SIGNING_KEY` | HMAC key used to sign session cookies. |
| `VARY_AUTH_TOKEN_HASH_SECRET` | Secret used to hash API tokens before storage. |
| `VARY_AUTH_SQLITE_BINDING` | Via add-on binding that supplies the auth SQLite store (typically `AUTH`). |
| `VARY_AUTH_SQLITE_SCHEMA_VERSION` | Schema version the runtime expects; mismatch is a preflight blocker. |

`vary app preflight my-api --target prod` reports the same blockers that
Via uses to gate a deploy. Common preflight failure IDs:

| ID | Meaning |
|---|---|
| `auth_mode_missing` | Route requires authentication but `VARY_AUTH_MODE` is not set. |
| `auth_mode_invalid` | `VARY_AUTH_MODE` is not `oidc` or `jwt`. |
| `auth_oidc_provider_missing` | OIDC issuer/client variables are not all set. |
| `auth_session_signing_key_missing` | Session route requires `SESSION_SIGNING_KEY`. |
| `auth_token_hash_secret_missing` | API token verification requires `VARY_AUTH_TOKEN_HASH_SECRET`. |
| `auth_sqlite_binding_missing` | Auth store binding is not declared in the manifest. |
| `auth_capability_claim_missing` | Capability route is missing `VARY_AUTH_CAPABILITY_CLAIM`. |
| `auth_csrf_mode_invalid` | `VARY_AUTH_CSRF_MODE` is not one of the accepted values. |

Failed preflight items include the exact next command - typically a `vary
app env set` or `vary app config patch` invocation against the target.

## Audit and rate limits

Public mutation routes must emit an audit event and check a rate-limit key
before any state changes. Both come from `stdlib/access.vary`. The
rate-limit key is keyed on at least IP, user, route, action, and trust
level so a single hostile client cannot exhaust a route by rotating any one
dimension:

```text
import access

let key = access.rate_limit_key(
    request_ip, actor_user_id, "/api/v1/issues", "create", trust_level)
```

Audit events are written through the store with a stable event ID, an
actor, a target (kind + id), and a free-text description. The audit sink
redacts secrets and enforces line-length limits before persistence:

```text
self.store.record_audit_event(
    "audit-issue-create-" + issue_id,
    "issue_created",
    actor_user_id,
    "issue", issue_id,
    "created through /api/v1/issues",
    now)
```

Order matters. Validate the request first, then check the rate-limit key,
then write the audit event, then call domain logic. A failed validator
must not write an audit row for a malformed request, and the rate-limit
check must happen on the validated key so an attacker cannot escape it by
sending garbage.

## Error envelopes

Never let raw exceptions or string concatenation produce response bodies.
Use `import api_response`; the envelope helpers produce JSON with a stable
error `code`, a human `message`, and a request ID (`rid`) that the client
and the audit log can correlate on.

```text
import api_response

return api_response.validation_error("title", "must be 1-200 chars", rid)
return api_response.capability_error("forbidden", "issues.moderate", rid)
return api_response.error_object("not_found", "issue not found", rid)
```

Pick the envelope by the *kind* of failure, not by HTTP status. The status
code is set by the boundary based on the envelope:

| Failure | Envelope | When to use |
|---|---|---|
| A single field failed validation | `validation_error(field, message, rid)` | Decoder or `T.validate` rejected one named field. Lets the client highlight it. |
| Multiple fields, or shape-level failure (`required`, `at_least_one`) | `error_fields(code, message, field_errors, extra, rid)` | The decoder returns this when more than one field is wrong. |
| Caller lacks a capability | `capability_error(message, capability, rid)` | The authorizer denied the request. The named capability tells the client which scope to ask for. |
| Anything else - not found, conflict, generic refusal | `error_object(code, message, rid)` | Default for non-validation, non-authorization errors. |

`rid` should be the inbound request ID from `RequestRouteMetadata` so the
client, the audit row, and the structured log all share the same key. When
no request ID is available the default `"req_unavailable"` makes that
explicit instead of silently dropping the correlation.

`ManualResponseEnvelopeRule` (`VCH010`) flags hand-built envelopes; the
full surface is in
[stdlib](/docs/stdlib/).

## Compile-time safeguards

Each part of this page maps to a compile-time or check-rule safeguard. The
safeguards fall into two phases.

**Typecheck phase.** These run before any code is generated. They are
structural and produce errors, not warnings: the build cannot continue with a
known violation. Taint flow into unsafe sinks (`TaintTypeCheckerTest`),
markdown and content typing (`ContentTypeCheckerTest`, `VaryContentTest`),
and missing HTTP boundaries (`VaryHttpBoundaryTest`, `ExposeCodegenTest`) all
sit here.

**Check-rule phase.** `vary check` runs `VCx00x` rules against the source
after typecheck. These cover patterns that compile but are still wrong:
manual route catalogs (`VCA006`), manual handler adapters (`VCH010`), manual
projections and envelopes (`VCH009`, `VCH010`), markup string assembly
(`VCS001`), and outbound host denial (`VCS008`). Promote them to errors
in CI with `-W error` or `[check.rules]` in `vary.toml`.

| Concern | Phase | Safeguard |
|---|---|---|
| Tainted into sinks | typecheck | taint safety checker |
| Markdown/XSS corpus | typecheck | markdown and content typing |
| HTTP boundary present | typecheck | HTTP boundary validation |
| Auth and rate limits | app behavior | authorizer and rate-limit rules |
| Capability matrix | app behavior | declared capabilities |
| Audit redaction | app behavior | audit sink redaction |
| Outbound host allow-list | check rule | `ServiceHostDeniedRule` (`VCS008`), `ServiceServerDriftRule` (`VCA005`) |
| Manual route catalogs | check rule | `ManualRouteCatalogRule` (`VCA006`) |
| Manual handler adapters | check rule | `ManualHandlerAdapterRule` (`VCH010`) |
| Manual projections | check rule | `ManualProjectionRule` (`VCH009`) |
| Manual response envelopes | check rule | `ManualResponseEnvelopeRule` (`VCH010`) |
| Manual markup strings | check rule | `StructuredMarkupStringRule` (`VCS001`) |

## Checklist

When adding or changing a public route:

| Check | Requirement |
| --- | --- |
| Request fields | Every request field is either a typed shape field with `length`/`range`/`enum`/`validate`, or arrives as `Tainted[T]` and is validated before any sink. |
| Response shape | Response shape exists and is used; no hand-built envelopes. |
| Authorization | `auth` clause and any `capability` clauses match the threat model. |
| Mutations | Mutations carry `idempotent`, audit events, and a rate-limit key. |
| Markdown | Markdown goes through `MarkdownBody.validate` and `Markdown.render_body(...).safe_html()`. |
| Outbound URLs | Outbound URLs go through `UrlPolicy` or `PublicUrl.validate`. |
| Static checks | `vary check` passes with no manual-route-catalog, manual-handler, manual-projection, manual-envelope, or markup-string warnings. |
| OpenAPI | `vary routes ... --openapi` matches the checked-in OpenAPI artifact. |
