Alpha. Vary is under active development and not ready for production use. Syntax, APIs, performance, and behaviour may change between releases.

Public JSON APIs

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:

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:

InputValidator
Issue titlesIssueTitle.validate(raw)
Markdown bodiesMarkdownBody.validate(raw)
UsernamesUsername.validate(raw)
Email addressesEmailAddress.validate(raw)
LabelsLabelName.validate(raw)
URLsPublicUrl.validate(raw)
Search textSearchText.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:

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.

CommandOutput
vary routes src/contracts.vary --jsonRoute manifest
vary api spec src/contracts.vary --jsonCanonical compiler-owned model
vary api schema src/contracts.vary --jsonJSON schemas
vary api openapi src/contracts.vary --jsonOpenAPI document
vary api test-metadata src/contracts.vary --jsonGenerated test metadata
vary api docs-metadata src/contracts.vary --jsonDocumentation projections
vary api compat old.vary new.vary --jsonCompatibility evidence
vary api cli src/contracts.vary -o cli.varyVary 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.

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.

ClausePurpose
dependency name = exprA constant injected at service construction (a store, clock, config).
argument name = exprA per-call value supplied by the binding (e.g. clock.now()).
parameter name: TypeA typed value the compiler extracts from the request (body, query, route metadata).
returns exprThe 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:

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:

HelperReturns
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.

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).

MarkerMeaning
(default)Always emit; field is required.
?Nullable; omit the field when the value is None.
?alwaysNullable 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:

BlockRole
databaseSchema 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.
repositoryTyped 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.
queryTyped 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.

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:

TopicNote
Bindingbinding "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 namequery was previously called read_model; both spellings parse to the same AST, but new code should use query.
Manual SQLManual 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.

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:

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.

ModulePurpose
api_contextCarries 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_responseProduces 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.
mappingCovers the one-line list-to-list translations that turn query rows into contract values (map_rows, json_array_map).
pagingOwns cursor pagination. Fetch limit + 1 rows and hand them to page_slice; the result carries the page items and the next cursor.
projectionRuntime 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.
xmlHandles 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:

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:

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:

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

Publish the generated contract and client as release artifacts:

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.