
Use this checklist for public JSON APIs that are meant to be reachable from
untrusted clients.

## Secure route shape

Define an interface, implement it, and expose it through the generated HTTP
boundary:

```vary
import content

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

class IssueApiImpl() implements IssueApi {
    def create_issue(self, title: Tainted[Str], body: Tainted[Str]) -> Str {
        let title_str = title.validate_str(1, 160).unwrap()
        let body_str = body.validate_str(0, 65536).unwrap()
        let _valid_title = IssueTitle.validate(title_str).unwrap()
        let valid_body = MarkdownBody.validate(body_str).unwrap()
        let _safe = Markdown.render_body(valid_body, False).safe_html()
        return "{\"ok\":true}"
    }
}

expose IssueApi via http version "2026-05-10" stability stable
```

The generated resource receives raw JSON and `Content-Type`, calls the Vary
HTTP boundary, and only then invokes your service. Public request parameters
that should not be trusted use `Tainted[T]`.

## Validate tainted data

Use domain validators at the first trusted boundary:

| Input | Validator |
|---|---|
| Issue titles | `IssueTitle.validate(raw)` |
| Markdown bodies | `MarkdownBody.validate(raw)` |
| Usernames | `Username.validate(raw)` |
| Email addresses | `EmailAddress.validate(raw)` |
| Labels | `LabelName.validate(raw)` |
| URLs | `PublicUrl.validate(raw)` |
| Search text | `SearchText.validate(raw)` |

Do not pass tainted or raw public strings into SQL, shell execution, logs,
template rendering, Markdown rendering, URL parsing/building, or outbound HTTP.

## Route contracts

For public APIs, prefer an `api { ... }` contract with typed request and
response shapes. The compiler treats services inside the API block as the
canonical contract for routes, auth, cache, capabilities, idempotency,
pagination, OpenAPI, manifests, schema artifacts, tests, and generated CLIs:

```text
api IssueTrackerApi version "v1" base "/api/v1" stability stable {
    shape ListIssuesQuery {
        limit: Int? range 1..50
        page_token: Int?
    }

    shape CreateIssueRequest {
        required title, body
        title: Str length 1..200
        body: Str length 1..20000
    }

    response IssueListResponse {
        issues: List[IssueSummary]
        next_page_token: Int?
    }

    response IssueDetailResponse {
        issue: IssueDetail
    }

    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
            paginate cursor next_page_token
            cli command "issues list" table number, title, status

        post create_issue "/issues" body CreateIssueRequest -> IssueDetailResponse
            auth session
            idempotent
            cache private_no_store
            cli command "issues create" format json
    }
}
```

The `api` block is the single source of truth for the public surface.
Everything downstream - the OpenAPI document, the JSON schemas, the
generated client, the documentation projection, the compatibility check -
is derived from it by running one of the `vary api` subcommands. Each
subcommand reads the same contract file and emits a different machine-
readable view, so release artifacts cannot drift from the source.

| Command | Output |
|---|---|
| `vary routes src/contracts.vary --json` | Route manifest |
| `vary api spec src/contracts.vary --json` | Canonical compiler-owned model |
| `vary api schema src/contracts.vary --json` | JSON schemas |
| `vary api openapi src/contracts.vary --json` | OpenAPI document |
| `vary api test-metadata src/contracts.vary --json` | Generated test metadata |
| `vary api docs-metadata src/contracts.vary --json` | Documentation projections |
| `vary api compat old.vary new.vary --json` | Compatibility evidence |
| `vary api cli src/contracts.vary -o cli.vary` | Vary CLI from endpoint `cli` hints |

Each output carries endpoint names, methods, paths, parameter names,
request and response types, auth policy, cache policy, capabilities,
pagination, CLI projection hints, and any field policy metadata - whatever
the contract declared, the artifact knows about.

Do not rebuild public route catalogs with positional functions such as
`api_route_contract(index)`; it bypasses the contract and the drift gates
have nothing to compare against.

## Declarative handlers

Inside a `service { ... }` block, attach handlers from a module with `bind`
instead of writing adapter functions by hand. The compiler generates the route
adapter, decodes the request shape, supplies dependencies and arguments, and
checks the return type against the response shape.

```text
service IssueTrackerService at "https://issue-tracker.local" {
    get list_issues "/issues" query ListIssuesQuery -> IssueListResponse
    post create_issue "/issues" body CreateIssueRequest -> IssueDetailResponse

    bind create_issue from issue_api.mutations {
        parameter request: CreateIssueRequest
        parameter ctx: RequestRouteMetadata
        dependency store = store
        argument now = clock.now()
        returns api_response.ok_object("issue", issue_to_json(created))
    }

    bind handlers from issue_api.public_handlers except admin_export {
        dependency store = store
    }
}
```

The four clauses exist to keep three different lifetimes separate.
`dependency` values live as long as the service - they are resolved when the
service implementation is built and reused on every request. `argument`
values are recomputed per call, which is what you want for `clock.now()` or
freshly-issued IDs. `parameter` values come from the request itself: the
compiler matches the parameter type against the request body, query string,
and route metadata, decoding each field through the same machinery the api
shape uses. `returns` is the only clause that produces a value visible to the
client, and it is required - there is no implicit return.

| Clause | Purpose |
|---|---|
| `dependency name = expr` | A constant injected at service construction (a store, clock, config). |
| `argument name = expr` | A per-call value supplied by the binding (e.g. `clock.now()`). |
| `parameter name: Type` | A typed value the compiler extracts from the request (body, query, route metadata). |
| `returns expr` | The single expression that produces the response value. |

The wildcard form `bind handlers from module` binds every handler the module
exports, minus an optional `except` list. Use it once per service to avoid
hand-listing routes - the typical pattern is one wildcard binding for the
read side and individual `bind name from module` blocks for mutations that
need bespoke dependencies. Manual `def handle_*` adapters that route to
business logic by hand are flagged by `VCH010 ManualHandlerAdapterRule`,
because they sit outside the compiler's view of the route surface.

## Handler context

A handler bound through `bind ... from module` receives a `RequestContext`
when it declares `parameter ctx: RequestContext`. The context carries the
route metadata, the verified caller identity, the verified capability set,
the request ID, and the idempotency key:

```text
import api_context
import api_response

def create_issue(request: CreateIssueRequest, ctx: RequestContext, store: IssueStore, now: Instant) -> Str {
    let user_result = api_context.authenticated_user(ctx)
    if user_result.is_err() {
        return api_response.capability_error(
            "session required", "issues.write", ctx.request_id.value)
    }
    let csrf_result = api_context.require_csrf(ctx)
    if csrf_result.is_err() {
        return api_response.capability_error(
            "missing csrf token", "issues.write", ctx.request_id.value)
    }
    if not api_context.has_capability(ctx, "issues.write") {
        let _ = api_context.required_capability(ctx, "issues.write")
        return api_response.capability_error(
            "missing capability", "issues.write", ctx.request_id.value)
    }
    let actor = user_result.unwrap()
    let created = store.create(request, actor, now)
    return api_response.ok_object("issue", issue_to_json(created))
}
```

The helpers come from `import api_context`:

| Helper | Returns |
|---|---|
| `optional_user(ctx)` | `RequestUserId?` for `auth optional` routes; `None` when unauthenticated. |
| `authenticated_user(ctx)` | `Result[RequestUserId, ApiBoundaryError]` for routes that require a verified session. |
| `has_capability(ctx, name)` | `Bool` capability check that also requires `ctx.auth_verified`. |
| `required_capability(ctx, name)` | `Result[None, ApiBoundaryError]` that fails when the capability is absent. |
| `require_csrf(ctx)` | `Result[None, ApiBoundaryError]` that fails when CSRF was not verified. |
| `request_capabilities(ctx)` | `List[Str]` of all verified capabilities for diagnostics or projection policies. |

`RequestContext` also exposes the verified identity fields the runtime
populated from the session or API token: `principal_id`, `subject`,
`issuer`, `session_id`, `api_token_id`, `roles`, `auth_kind`,
`auth_verified`, and `csrf_verified`. Branch on them when a route is
`auth optional` and the response shape differs between anonymous and
authenticated callers.

## JSON mapping

Replace hand-built response objects with a `json` mapping declaration. The
compiler reads a source row type, applies an optional projection policy, and
emits the JSON object.

```text
json issue_summary from IssueRow as row projected by public_field_policy {
    id: row.id
    title: row.title
    severity: row.severity
    deleted_at? if row.deleted_at != None: row.deleted_at
    body? always: row.body
}
```

The per-field markers exist because public APIs need three different rules
for missing data and the JSON encoder cannot guess which one applies. By
default a field is required and always present. `?` says the value is
nullable and the field disappears from the response when it is `None` -
useful for fields that simply do not exist for some rows. `?always` keeps
the field but writes a literal `null`, which is what you want when a client
relies on the field's presence as a signal. `if <expr>` is a conditional
emit driven by application state (e.g. only include `private_notes` when the
caller has the `issues.moderate` capability).

| Marker | Meaning |
|---|---|
| (default) | Always emit; field is required. |
| `?` | Nullable; omit the field when the value is `None`. |
| `?always` | Nullable but always emit (writes `null` when `None`). |
| `if <expr>` | Emit only when the predicate is true. |
| `{ ... }` | Nested JSON object. |

`projected by <policy>` references a `ProjectionPolicy` from `import
projection` and acts as a top-level allow-list of fields: any field not
listed in the policy is omitted regardless of its per-field marker. That
makes the policy the single place to read when answering "which fields does
this surface expose?" instead of scanning every mapping. The mapping itself
is consumed by handler bindings and the generated client. Manual `def
to_json` projections are flagged by `VCH009 ManualProjectionRule`; XML and
HTML markup string assembly is flagged by `VCS001 StructuredMarkupString` -
use `import xml` helpers or the markdown safe-HTML pipeline instead.

## Declarative persistence

`database`, `repository`, and `query` blocks declare the storage layer the
same way `api`/`service` declare the HTTP surface. They cover three different
concerns that all end up as parameterized SQL:

| Block | Role |
| --- | --- |
| `database` | Schema and migration history. There is exactly one per binding, it carries a version number, and migrations are append-only. The runtime uses the version to refuse a release whose code disagrees with the deployed schema. |
| `repository` | Typed CRUD against one table. Inserts, updates, deletes, and counts get their own typed function; the compiler generates the SQL and binds parameters by name. This is the right home for writes and for simple count/exists reads. |
| `query` | Typed reads, either a structured `from / where / order / select` block or a raw `sql "..." [params]` block with a typed result row. Use it for anything that joins, aggregates, or projects across more than one table. |

The split matters because it puts each thing in its natural home: schema
changes touch `database`, writes touch `repository`, and reads touch
`query`. You never have raw SQL strings spread across the codebase, which is
the whole point - the `DslReplaceableSqlRule` and `ManualRowMappingRule`
flag manual `db.execute(...)` calls so any new persistence work picks the
DSL by default.

```text
database AppDb version 3 binding "DATABASE" {
    table issues
    migration 1 "initial" {
        create_table issues (
            id Int primary,
            title Str,
            severity Str,
            created_at Int
        )
    }
}

repository IssuesRepo for issues {
    insert insert_issue(title: Str, severity: Str, created_at: Int) -> Int
    delete delete_issue(id: Int)
    count count_open_issues() where severity != "closed"
}

query IssueRow list_issues(limit: Int) {
    from issues
    order { issues.created_at desc }
    limit limit
    select { id, title, severity }
}
```

Notes:

| Topic | Note |
| --- | --- |
| Binding | `binding "DATABASE"` matches the Via add-on binding name. The runtime descriptor is supplied by the config server; there is no host path in source. |
| Query name | `query` was previously called `read_model`; both spellings parse to the same AST, but new code should use `query`. |
| Manual SQL | Manual SQL strings for simple CRUD are flagged by `DslReplaceableSqlRule` and `ManualRowMappingRule`. |

## Service implementations

A `service` block describes the contract; a `service implementation` block
binds it to a specific set of dependencies. They are deliberately separate
so the contract can live next to the api shape (with the OpenAPI, the
schemas, and the generated client) while the implementation lives in the
module that actually owns the data.

```text
service implementation IssueTrackerService by issue_api.handlers {
    dependency clock: Clock = system_clock()
    dependency store: IssuesRepo = issues_repo()
    dependency policy: ProjectionPolicy = public_field_policy
}
```

That split buys three things. Test code can declare a second
`service implementation` for the same `service` with `memory_store()` and
a frozen clock - no boundary mocking required. Production and dev can ship
different implementations for the same contract (e.g. a real SMTP gateway
vs. an in-memory capture) without forking the route declarations. And the
generated client and OpenAPI artifacts depend only on the contract, so
swapping the implementation never produces a release-artifact diff.

The compiler injects each dependency into every `bind` block in the
service, so handler bodies stay focused on the request itself - no
constructor wiring, no service locator, no global state.

Long-running Via apps serve a typed contract by naming the contract service
from the lifecycle block:

```text
service IssueTracker {
    http IssueTrackerService
    worker audit_retention run audit_retention_worker required drain
    health "/health"
    readiness "/ready"
}
```

`http IssueTrackerService` is checked as a real server binding. The named
service must be a typed `service ... at ...` contract with a matching
`service implementation`; otherwise the compiler and Via preflight report
`http_service_binding_missing` before traffic is routed.

## Stdlib support

The DSLs above lean on six small stdlib modules. The split exists so each
piece has one obvious home: the language declares the shape, the stdlib
supplies the values that fill it.

| Module | Purpose |
| --- | --- |
| [`api_context`](/docs/stdlib/) | Carries typed request metadata. `RequestRouteMetadata`, `RequestId`, `IdempotencyKey`, `SessionToken`, and the assorted ID types (`CommentId`, `ReportId`, etc.) are declared here so handler bindings can name them as `parameter` types instead of accepting raw `Str` IDs. |
| [`api_response`](/docs/stdlib/) | Produces the JSON envelopes. `ok_object`, `ok_page`, and the `error_*` family return strings that the boundary writes directly to the response; the `ManualResponseEnvelope` check rule flags anything that builds these shapes by hand. |
| [`mapping`](/docs/stdlib/) | Covers the one-line list-to-list translations that turn query rows into contract values (`map_rows`, `json_array_map`). |
| [`paging`](/docs/stdlib/) | Owns cursor pagination. Fetch `limit + 1` rows and hand them to `page_slice`; the result carries the page items and the next cursor. |
| [`projection`](/docs/stdlib/) | Runtime backing for `json … projected by …`. Build a `ProjectionPolicy` once per surface and reuse it across mappings so the field visibility rules live in one place. |
| [`xml`](/docs/stdlib/) | Handles XML and Atom output. Use the builders (`xml_element`, `xml_text_element`, `xml_empty_element`) for feed responses; string-concatenated markup is flagged by the `StructuredMarkupString` rule. |

## Capabilities and abuse controls

Declare and test the capabilities a public route needs on the endpoint and in
the domain authorization path. Issue tracker routes typically need app-specific
capabilities such as `issues.private.read`, `issues.moderate`,
`issues.status.update`, `issues.labels.update`, `issues.assignees.update`,
`audit.read`, and `webhooks.write`. Public mutation routes should also apply
auth checks, rate-limit keys, audit events, and stable error envelopes before
calling core domain logic.

Host-bound outbound calls belong in typed `service Name at "https://..."` client
declarations and should be covered by service-host authority checks.

## Managed SQLite

Use managed SQLite migrations before runtime startup:

```vary
let schema = Sql.schema(2)
Sql.migrate_binding("DATABASE", schema).close()
let _db = Sql.connect_managed("DATABASE", schema)
```

For local tests, prefer `Sql.memory()` or a temp file. In Via, `DATABASE` is a
binding name. The platform supplies the runtime descriptor and owns snapshot,
restore, and backup behavior.

## Workers

Long-running app services use `service Name { ... }` declarations. Declare
workers, shutdown behavior, and readiness checks so Via can expose lifecycle
metadata and drain safely during deploy or rollback.

## Safe Markdown

Render public Markdown only after validating it as `MarkdownBody`. The trusted
HTML sink takes `SafeHtml` from `Markdown.render_body(...).safe_html()`. Raw
HTML, script tags, event handlers, unsafe protocols, and unsafe attributes are
sanitized by the runtime-backed content module.

## Versioned API Contract

API declarations can define request and response DTO shapes:

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

    enum WebhookEvent = ["issue_created", "issue_updated"]

    shape CreateCommentRequest {
        required body
        body: Str length 1..20000
    }

    shape UpdateWebhookRequest {
        at_least_one url, events, enabled
        url: Str? length 8..2048 validate PublicWebhookUrl
        events: List[WebhookEvent]? default ["issue_created"]
        enabled: Bool?
    }

    response RegistrationResponse {
        ok: Bool
        message: Str
    }
}
```

If a matching `data` type exists, `vary check` validates that the API shape does
not drift from the data fields. If no matching `data` type exists, the compiler
generates a data DTO with `from_json`, `decode`, `decode_json`, `to_json`, and
`to_json_list` helpers. Generated decoders enforce declarative `length`,
`range`, inline and named enum values, reusable field validators, defaults, and
shape-level `at_least_one` constraints before domain-specific validation runs.
OpenAPI output includes named validator metadata and item enums for
`List[Enum]` fields.
Generated projection helpers let response shapes become JSON without rebuilding
each field by hand:

```text
let response = RegistrationResponse(True, "created")
return RegistrationResponse.to_json(response).stringify()
```

Publish the generated contract and client as release artifacts:

```bash
vary routes src/app.vary --openapi > docs/api/my-api/openapi.json
vary client src/app.vary --language typescript > docs/api/my-api/client.ts
```

Keep a drift test or checked hash for the route manifest and OpenAPI output.
Compatibility checks should treat removed routes, route-shape changes, auth or
cache policy changes, capability changes, parameter type changes, and response
type changes as breaking.
