{"protocol":"clank-doc/1","frameworkVersion":"0.9.4","slug":"provider-runtime-ingress","title":"Provider runtime ingress","description":"@clank.run/framework/provider runtime is the provider private traffic boundary between Clank managed ingress and application runtimes. It is zero dependency, provider neutral, and designed to sit beside a Docker, VM, microVM, Nomad, or Kube","group":{"id":"deploy","title":"Deploy and operate"},"url":"https://docs.clank.run/docs/provider-runtime-ingress","source":"docs/provider-runtime-ingress.md","headings":["Create the registry","Activate one generation","End to end request binding","Overlap, switch, and drain","Process and data lifecycle","Shutdown"],"tableOfContents":[{"id":"create-the-registry","title":"Create the registry","level":2},{"id":"activate-one-generation","title":"Activate one generation","level":2},{"id":"end-to-end-request-binding","title":"End-to-end request binding","level":2},{"id":"overlap-switch-and-drain","title":"Overlap, switch, and drain","level":2},{"id":"process-and-data-lifecycle","title":"Process and data lifecycle","level":2},{"id":"shutdown","title":"Shutdown","level":2}],"markdown":"# Provider runtime ingress\n\n`@clank.run/framework/provider-runtime` is the provider-private traffic boundary between Clank\nmanaged ingress and application runtimes. It is zero-dependency, provider-neutral, and designed to\nsit beside a Docker, VM, microVM, Nomad, or Kubernetes launcher.\n\nIt solves one narrow problem completely: a request for project A, release B, and generation C can\nreach only the loopback runtime explicitly published for that exact binding.\n\n## Create the registry\n\n```ts\nimport {\n  createDeploymentRuntimeIngress,\n} from \"@clank.run/framework/provider-runtime\";\n\nconst runtimeIngress = createDeploymentRuntimeIngress({\n  timeoutMs: 30_000,\n  maxBodyBytes: 25 * 1024 * 1024,\n  maxUrlLength: 8 * 1024,\n  maxBindings: 10_000,\n  onError(error) {\n    privateProviderLog(error);\n  },\n});\n```\n\nExpose `runtimeIngress.handle(request)` only through the trusted provider HTTP service used by the\nmanaged data plane. Terminate TLS before that handler and restrict network admission to the Clank\nedge whenever possible. Application listeners stay private on `127.0.0.1` or `[::1]`; the\nregistry rejects a remote, HTTPS, credential-bearing, path-bearing, query-bearing, or fragment\nupstream.\n\n## Activate one generation\n\nAfter the provider has staged code and data, launched an isolated candidate, and checked it\ndirectly over loopback, publish its capsule binding:\n\n```ts\nconst state = await runtimeIngress.activate({\n  protocol: \"clank-runtime/1\",\n  projectId: prepared.projectId,\n  releaseId: prepared.releaseId,\n  generation: prepared.generation,\n  path: prepared.ingress.route,\n  token: prepared.ingress.token,\n  upstream: candidate.url, // for example http://127.0.0.1:4617\n});\n```\n\nActivation validates an exact plain object and a safe, unencoded provider path. It computes the\nroute token's SHA-256 digest and retains no plaintext token. `inspect()` returns only project,\nrelease, generation, provider path, activation time, in-flight count, and whether that is the\nlatest accepted generation. It never returns the token digest or loopback origin.\n\nAn exact activation retry is idempotent only while that binding remains published. A conflicting\ncopy of the same generation, a deactivated or lower generation, a cross-project path collision, or\na capacity overflow fails closed. An activation already validating its token cannot publish after\n`close()` begins. The same configured ceiling bounds both remembered project high-water marks and\nthe combined published/draining generation set.\n\nAfter a confirmed permanent project deletion has removed its provider-owned runtime and data,\n`forget(projectId, generation)` can release that inactive project's exact high-water entry. It\nrefuses while any generation for the project is published or draining and ignores a mismatched\ngeneration, so a stale cleanup cannot free a live identity.\n\n## End-to-end request binding\n\nThe public managed data plane fixes the provider origin and path, removes client-controlled\nbinding headers, and sends:\n\n- `x-clank-project-id`;\n- `x-clank-runtime-protocol`;\n- `x-clank-runtime-generation`; and\n- `x-clank-runtime-ingress`.\n\nThe provider registry selects a published path and exact project/generation before hashing the\nsupplied token in constant-work comparison. Missing paths, invalid headers, stale generations, and\nwrong tokens all return the same generic `503 RUNTIME_UNAVAILABLE` response without calling an\napplication.\n\nAfter successful validation, the provider:\n\n- retains an in-flight lease before asynchronous token verification, closing the\n  activation/authentication race;\n- strips all binding and hop-by-hop headers before calling the application;\n- builds the target from the fixed loopback origin before assigning the public path suffix;\n- preserves application authorization, cookies, forwarding headers, query strings, and streaming\n  responses;\n- bounds URL and request-body intake, applies a request deadline, and refuses redirects; and\n- strips private server and binding headers from the application response.\n\nThe route credential therefore authenticates the edge-to-provider hop. It is never forwarded into\napplication code, placed in a URL, returned in inspection, or included in a public failure.\n\n## Overlap, switch, and drain\n\nNew and old generations of one project may be published together, including on the same provider\npath. The trusted generation header chooses between them. This permits a code-only or\nexternally-stateful rollout to avoid a distributed switch gap:\n\n1. launch and health-check generation 12;\n2. `activate()` generation 12 while generation 11 remains published;\n3. atomically switch managed ingress to generation 12;\n4. call `deactivate(projectId, 11)`;\n5. wait for its returned `drained` result, then stop generation 11.\n\n`deactivate()` removes the exact generation from new routing before waiting for response streams\nalready assigned to it. A retry waits for the same in-progress drain instead of falsely reporting\nsuccess. A stale generation cannot revoke the latest one. `drain()` only waits; use it after\nanother boundary has stopped assigning new traffic. A timeout returns `drained: false`, but retains\nthe generation until its final stream ends; it cannot be forgotten or reactivated during that\ninterval.\n\nSQLite restore and migration are stopped-runtime operations in the packaged\n[provider data lifecycle](provider-data-lifecycle.md). Do not keep an old runtime writing the same\ndatabase merely to achieve overlap. Revoke and drain it first, accept the resulting maintenance\nwindow, or use a data architecture that safely supports concurrent versions. Availability never\njustifies corrupting project data.\n\n## Process and data lifecycle\n\nThis registry intentionally does not:\n\n- launch or sandbox application processes;\n- persist environment values or route tokens;\n- mutate provider SQLite data;\n- decide placement or node ownership;\n- publish the control-plane edge route; or\n- issue TLS certificates.\n\nA complete provider composes it with\n`openDeploymentProviderDataStore()`, the package-supported\n`openDockerDeploymentRuntimeLauncher()` or a stronger isolated launcher, the standard deployment\nprovider, and the runner agent. Keep the prepared environment and plaintext ingress token\nmemory-only. Publish traffic only after the provider data commit succeeds.\n\n`openDockerDeploymentProviderService()` is the package-supported composition. It persists the\ndesired operation/fence, drains before writer shutdown, defers jobs until data commit, activates\nthis ingress last, and cleans a failed candidate in reverse order. Use the lower-level registry\ndirectly only when implementing another runtime boundary.\n\nOn provider restart the in-memory registry begins empty and returns generic unavailable responses.\nThe Docker reference launcher likewise removes exact-owner orphan containers instead of adopting\nthem. The hosted activation integration must reset/reconcile the node's desired generation before\nrestoring traffic; it must not infer a live process from old control-plane observation alone.\n\n## Shutdown\n\n`close()` atomically revokes every published generation and waits up to the supplied deadline for\nassigned streams. No new activation is accepted afterward. This makes a provider service shutdown\nfail closed while leaving process termination policy with the launcher.\n\nContinue with [Managed ingress and external data](data-plane.md),\n[Provider data lifecycle](provider-data-lifecycle.md),\n[Provider Docker runtime](provider-docker-runtime.md),\n[Complete deployment provider service](provider-service.md),\n[Deployment provider adapters](provider-adapters.md), and\n[Remote runtime placement](runtime-placement.md).\n"}