Capability Binding
Capability binding is the mechanism that connects your compiled spec's capability requirements to concrete provider implementations at deploy time.
Why this exists
Without an explicit binding model, provider selection is implicit and unreproducible. Two deployments from the same artifact can activate different providers. A missing provider is discovered at runtime when a user triggers an affected code path. There is no reviewable artifact that records which provider satisfies which port.
Capability binding makes provider selection explicit, reproducible, and enforced. A missing provider is an activation failure, not a runtime error.
The three-phase pipeline
Capability binding is a three-phase pipeline.
Phase 1: Requirement discovery (compile time)
compileApp() → CapabilityBindingRequirements
Phase 2: Resolution (deploy time)
resolveTrustedProviders() → CapabilityBindingPlan + lockfile
Phase 3: Enforcement (runtime startup)
createAppRuntime() → validates lockfile → activates providers
Phase 1: Requirement discovery
compileApp() emits a CapabilityBindingRequirements artifact alongside the CompiledEntrypointBundle. This artifact is vendor-neutral: it lists port names and semantics, never provider names.
{
"$schema": "CapabilityBindingRequirements@1.0.0",
"required": [
{ "port": "collections.write", "semantics": "durable-write" },
{ "port": "collections.read", "semantics": "durable-read" },
{ "port": "ids.generate", "semantics": "id-generation" },
{ "port": "session.write", "semantics": "session-store" },
{ "port": "auth.sign_in", "semantics": "auth" }
]
}
Commit this artifact to version control. When a spec change adds a new capability requirement, the diff makes that visible in code review before you deploy.
Phase 2: Resolution
The resolver takes your CapabilityBindingRequirements and a set of candidate providers and produces a CapabilityBindingPlan plus a lockfile.
import { resolveTrustedProviders } from "@gooi/app-marketplace/resolve";
const { plan, lockfile } = await resolveTrustedProviders({
requirements,
candidates: [
{ provider: "@my-org/pg-provider", version: "^0.3.0" },
{ provider: "@gooi-marketplace/supabase", version: "^1.0.0" },
{ provider: "@gooi-marketplace/memory", version: "^1.0.0" },
],
});
The lockfile records exactly which provider satisfies each port, at which version, with which hash.
{
"$schema": "CapabilityBindingLockfile@1.0.0",
"resolvedAt": "2026-03-02T00:00:00Z",
"bindings": [
{
"port": "collections.write",
"provider": "@my-org/pg-provider",
"version": "0.3.1",
"hash": "sha256:abc123...",
"semantics": "durable-write"
},
{
"port": "auth.sign_in",
"provider": "@gooi-marketplace/supabase",
"version": "1.2.0",
"hash": "sha256:def456...",
"semantics": "auth"
}
],
"unresolved": []
}
Commit the lockfile to version control. Two deployments with the same lockfile activate the same providers at the same versions. This is the reproducibility guarantee.
A lockfile with a non-empty unresolved array will pass resolution but fail at runtime activation. Resolve all ports before deploying.
Phase 3: Enforcement
createAppRuntime() accepts the lockfile as a required input. At startup, the kernel:
- Validates the lockfile against the bundle's
CapabilityBindingRequirements. Any unresolved required port is a startup failure. - Loads each provider module using the module-loader host port.
- Validates each loaded module's hash against the lockfile entry. A hash mismatch is a startup failure.
- Activates each provider.
- Only after all providers are activated does the kernel accept invocations.
import { createAppRuntime } from "@gooi/app-runtime/create";
const runtime = createAppRuntime({
bundle,
lockfile,
hostPorts,
});
If any required capability port has no binding, the runtime throws at startup. This is by design. A runtime that accepts invocations with unbound ports would silently fail when users hit affected code paths.
Swapping providers
Swapping providers requires changing the lockfile only. Your spec and application code do not change.
# Re-run resolution with new candidates
bun run resolve --candidates @new-org/better-provider@^2.0.0
# Commit the new lockfile
git add gooi.lockfile.json && git commit -m "bind: switch to better-provider for collections"
The spec never knew which provider was selected. The swap is a deploy-time decision, not an application code change.
What's next
- Control Plane: generate deterministic report/plan/apply/drift/generate outputs from compiled + bound artifacts
- The Marketplace: discover providers and understand trust tiers
- Building a Provider: implement capability ports yourself
- L2: Embedded Runtime: pass your lockfile to
createAppRuntime()