# Bindings Guide

Every Cloudflare resource the Worker talks to — a database, a KV namespace, a bucket, a queue — arrives as a _binding_
on the per-request `Env` object. This guide covers the accessor classes in
[`src/core/bindings/`](../../src/core/bindings/) that wrap them, and how to wire each one in `wrangler.json`. How their
settings are chosen is
[Register the configuration surface](../getting-started/quick-start.md#2-register-the-configuration-surface)'s job.

Four places are involved, and the split is deliberate:

| Where                                                          | Holds                                                                     |
| -------------------------------------------------------------- | ------------------------------------------------------------------------- |
| `@bayudwiyansatria/core`                                       | `configure` and `resolve` — the one abstraction settings are read through |
| [`src/constants/Defaults.ts`](../../src/constants/Defaults.ts) | `cloudflareDefaults` — the default binding name and settings for each     |
| [`src/core/bindings/`](../../src/core/bindings/)               | One `*Binding` accessor per Cloudflare binding, over the `Binding` base   |
| your Worker's `config/`                                        | Overrides — plain data replacing individual default fields                |

## Why an accessor class

Bindings live on `ctx.env`, not on a module-level import, so they cannot be captured once at startup. The accessor holds
the _name_ of a binding — handed to it already resolved — and reads the binding from the `Env` given to each call:

```ts
import { KVBinding } from '@bayudwiyansatria/cloudflare'

// Created once, at module scope. Binding name and default TTL come from the configuration layer.
const cache = new KVBinding()

// The binding itself is read per request.
api.get('/profile/:id', async ctx => {
  const profile = await cache.getJson<Profile>(ctx.env, `profile:${ctx.req.param('id')}`)
  return ctx.json(profile)
})
```

What that buys over reaching into `ctx.env.KV` directly:

- **A named failure.** A binding that was never declared in `wrangler.json` throws `MissingBindingError` naming the
  binding, instead of `Cannot read properties of undefined`.
- **Settings in one place.** TTLs, models, queue delays, and `topK` values are configured once, in the configuration
  layer, rather than repeated at every call site — and overridable from `config/` without editing `core/`.
- **Renaming stays local.** Change the binding name in one override and every call site follows.

## Available bindings

| Class                      | Config key          | Default binding       | Wraps                    | `wrangler.json` key         |
| -------------------------- | ------------------- | --------------------- | ------------------------ | --------------------------- |
| `AIBinding`                | `ai`                | `AI`                  | `Ai`                     | `ai.binding`                |
| `AiSearchBinding`          | `aiSearch`          | `AI_SEARCH`           | `AiSearchInstance`       | `ai_search`                 |
| `AiSearchNamespaceBinding` | `aiSearchNamespace` | `AI_SEARCH_NAMESPACE` | `AiSearchNamespace`      | `ai_search_namespaces`      |
| `AnalyticsBinding`         | `analytics`         | `TELEMETRY`           | `AnalyticsEngineDataset` | `analytics_engine_datasets` |
| `BrowserBinding`           | `browser`           | `BROWSER`             | `Fetcher`                | `browser`                   |
| `D1Binding`                | `d1`                | `DB`                  | `D1Database`             | `d1_databases`              |
| `DurableObjectBinding`     | `durableObject`     | `DO`                  | `DurableObjectNamespace` | `durable_objects.bindings`  |
| `FlagshipBinding`          | `flagship`          | `FLAGS`               | `Flagship`               | `flagship`                  |
| `HyperdriveBinding`        | `hyperdrive`        | `HYPERDRIVE`          | `Hyperdrive`             | `hyperdrive`                |
| `KVBinding`                | `kv`                | `KV`                  | `KVNamespace`            | `kv_namespaces`             |
| `PipelineBinding`          | `pipeline`          | `PIPELINE`            | `Pipeline`               | `pipelines`                 |
| `QueueBinding`             | `queue`             | `QUEUE`               | `Queue`                  | `queues.producers`          |
| `R2Binding`                | `r2`                | `BUCKET`              | `R2Bucket`               | `r2_buckets`                |
| `RateLimitBinding`         | `rateLimit`         | `RATE_LIMITER`        | `RateLimit`              | `ratelimits`                |
| `VectorizeBinding`         | `vectorize`         | `VECTORIZE`           | `Vectorize`              | `vectorize`                 |
| `VpcServiceBinding`        | `vpcService`        | `VPC_SERVICE`         | `Fetcher`                | `vpc_services`              |
| `VpcNetworkBinding`        | `vpcNetwork`        | `VPC_NETWORK`         | `VpcNetwork`             | `vpc_networks`              |
| `WorkflowBinding`          | `workflow`          | `WORKFLOW`            | `Workflow`               | `workflows`                 |

The default binding names come from [`src/constants/Defaults.ts`](../../src/constants/Defaults.ts); any of them can be
pointed elsewhere from `config/`.

`SecurityMiddleware` is configured the same way (key `security`) but wraps no binding — it applies CORS, IP capture, and
API-key auth to the Hono app. See [`SecurityMiddleware`](../../src/middlewares/SecurityMiddleware.ts).

All of the accessors are re-exported from the barrel:

```ts
import { D1Binding, KVBinding, R2Binding } from '@/core/bindings'
```

## Capability services

Business code does not use these accessors directly. Each one is wrapped by a capability service in
[`src/core/services/`](../../src/core/services/), which is what a Worker's own `services/` actually calls:

| Capability             | Offers                                                         | Returns                        |
| ---------------------- | -------------------------------------------------------------- | ------------------------------ |
| `AIService`            | `ask`, `embed`, `logId`                                        | text, vectors                  |
| `AnalyticsService`     | `request`, `event` — telemetry only, never logs                | `boolean`                      |
| `D1Service`            | `all`, `first`, `write`, `batch`                               | rows, `{ changes, lastRowId }` |
| `DurableObjectService` | `call`, `create`                                               | the object's parsed reply      |
| `HyperdriveService`    | `connectionString`, `credentials`, `target`                    | connection details             |
| `KVService`            | `get`, `set`, `remove`, `remember`, `keys`                     | the cached value               |
| `QueueService`         | `enqueue`, `enqueueAll`, `backlog`                             | `boolean`, metrics             |
| `R2Service`            | `upload`, `uploadJson`, `download`, `exists`, `remove`, `list` | bodies, `{ key, size }`        |
| `RateLimitService`     | `admit`                                                        | `boolean`                      |
| `VectorizeService`     | `index`, `search`, `similar`, `remove`                         | matches, mutation ids          |

Eight accessors have no service beside them, for three different reasons. `BrowserBinding` would need
`@cloudflare/puppeteer` to offer anything beyond the binding itself, and this package does not put a browser automation
library in the dependency tree of a Worker that installed it for a KV accessor. `WorkflowBinding` has no kernel
capability to implement — the kernel names a `CacheStore`, a `DataStore`, a `MessageQueue`, and durable execution is not
among them. `FlagshipBinding` has no kernel capability either, though it is the likeliest of the three to earn one: flag
evaluation has many providers and none of its vocabulary is Cloudflare's. The two AI Search accessors are in the same
position — managed retrieval is not among the kernel's capabilities either, and neither is record archival, which is
what leaves `PipelineBinding` and the two Workers VPC accessors in the same place. Every one of the eight says so in its
own documentation, so the asymmetry does not read as an oversight.

They return plain values rather than `APIResponse` — that contract belongs at the HTTP boundary, not inside the platform
layer — and every one answers `isAvailable(env)` so a caller can degrade when an optional resource is not bound.

```ts
import { KVService } from '@bayudwiyansatria/cloudflare'

const cache = new KVService()

const article = await cache.remember(env, `article:${id}`, () => load(env, id), 600)
```

See the [Quick Start](../getting-started/quick-start.md) for how a route, a business service, and these capabilities fit
together.

## Shared API

Every accessor extends [`Binding`](../../src/core/bindings/Binding.ts) and inherits:

| Member            | Behaviour                                                           |
| ----------------- | ------------------------------------------------------------------- |
| `name`            | The binding name this instance resolves                             |
| `isBound(env)`    | `true` when the binding is present — use it to degrade gracefully   |
| `resolve(env)`    | The binding itself; throws `MissingBindingError` when it is missing |
| `tryResolve(env)` | The binding, or `null` when it is missing                           |

Every helper method takes `env` as its first argument and calls `resolve` internally, so a missing binding always fails
the same way.

## D1

```json
"d1_databases": [{ "binding": "DB", "database_name": "app", "database_id": "<id>" }]
```

```ts
const db = new D1Binding()

const user = await db.first<User>(ctx.env, 'SELECT * FROM users WHERE id = ?', id)
const active = await db.all<User>(ctx.env, 'SELECT * FROM users WHERE active = ?', 1)
const result = await db.run(ctx.env, 'UPDATE users SET name = ? WHERE id = ?', name, id)

await db.batch(ctx.env, [
  db.prepare(ctx.env, 'INSERT INTO audit (action) VALUES (?)', 'rename'),
  db.prepare(ctx.env, 'UPDATE users SET name = ? WHERE id = ?', name, id)
])
```

Bind values are always passed separately from the SQL — `exec` is the one path without binding support, so keep it for
migrations and never build a statement there from user input.

## KV

```json
"kv_namespaces": [{ "binding": "KV", "id": "<id>" }]
```

```ts
const cache = new KVBinding() // expirationTtl: 300 by default

await cache.putJson(ctx.env, 'session:42', { userId: 42 }) // expires in 300s
await cache.putJson(ctx.env, 'flag:beta', true, { expirationTtl: 86400 }) // per-call override
const session = await cache.getJson<Session>(ctx.env, 'session:42')
const page = await cache.list(ctx.env, { prefix: 'session:' })
```

`expirationTtl` from the constructor is applied only when a call sets neither `expirationTtl` nor `expiration`.

## R2

```json
"r2_buckets": [{ "binding": "BUCKET", "bucket_name": "app-assets" }]
```

```ts
const storage = new R2Binding()

await storage.put(ctx.env, 'reports/2026-07.csv', csv)
await storage.putJson(ctx.env, 'reports/2026-07.json', report) // sets application/json
const csvBody = await storage.getText(ctx.env, 'reports/2026-07.csv')
const listing = await storage.list(ctx.env, { prefix: 'reports/' })
```

`get` returns the object with its body; `head` returns metadata only, which is the cheaper check for existence.

## Queues

```json
"queues": { "producers": [{ "binding": "QUEUE", "queue": "jobs" }] }
```

```ts
const jobs = new QueueBinding<EmailJob>()

await jobs.send(ctx.env, { to: 'user@example.com', template: 'welcome' })
await jobs.sendBatch(ctx.env, pendingJobs)
```

`sendBatch` bills as a single write regardless of how many messages it carries — prefer it in loops.

## Pipelines

```json
"pipelines": [{ "binding": "PIPELINE", "pipeline": "raw-records" }]
```

```ts
const archive = new PipelineBinding<RawRecord>()

// The only copy — a failure should be heard.
await archive.send(ctx.env, records)

// A second sink beside the real one — a failure must not fail the run.
await archive.sendSafe(ctx.env, rejected)
```

**Two methods, and choosing between them is the whole decision.** `send` throws like every other accessor here.
`sendSafe` logs and returns `false`, exactly as `AnalyticsBinding.writeSafe` does, because a pipeline's usual job is a
second, independent sink beside the one doing the real work — archiving the raw record a provider returned while the
coerced one goes on to storage. A sink like that failing the run that produced the records inverts its purpose: the
archive exists to explain a bad run and would instead be causing one. Ask whether the record has anywhere else to be. An
audit trail that is the only copy should throw.

`send` resolves when the pipeline has _accepted_ the records, not when they are written — the batching is what happens
after this returns, so a successful send is a promise to land them rather than evidence that it happened.

An empty array is passed through rather than short-circuited: whether an empty batch is worth a call is the pipeline's
business, and a caller that filtered everything out has already decided to send.

### Against R2 and Queues

Writing to [R2](#r2) directly means the producer owns buffering — how many records make a file, how long to wait through
a slow hour, what happens to a partial batch when the isolate is evicted. Ingestion that arrives in bursts is exactly
where that goes wrong, and it goes wrong quietly. A pipeline moves that policy onto the destination, where it applies to
every producer.

It is not [Queues](#queues) either, though both take a batch and return quickly. A queue exists so another Worker can
act on each message; a pipeline exists so nothing has to. If the records will be read as files later rather than
processed one at a time, a queue's consumer is a step whose only job is writing them down.

### The transformation entrypoint stays in your Worker

A pipeline can run records through a `PipelineTransformationEntrypoint` before landing them. Like a Workflow's
`WorkflowEntrypoint`, that is a class the consuming Worker exports and names in its own `wrangler.json`, so this package
wraps the producer side only — a base class that must be exported from your entry has nothing an adapter can add.

## Workers AI

```json
"ai": { "binding": "AI" }
```

```ts
const ai = new AIBinding() // uses the configured model or the package default

const answer = await ai.run(ctx.env, { prompt: 'Summarise this changelog' })
const embedding = await ai.run(ctx.env, { text: [body] }, '@cf/baai/bge-base-en-v1.5')
```

The first call uses the model from the `ai` configuration, which defaults to `@cf/meta/llama-3.1-8b-instruct`. Override
`ai.model` during `configure()` to choose another deployment-wide default. The second call names its model explicitly,
which always wins. Set `gateway` in the same configuration override to route calls through an AI Gateway, then read
`logId(ctx.env)` to correlate a response with its gateway log entry.

## Vectorize

```json
"vectorize": [{ "binding": "VECTORIZE", "index_name": "documents" }]
```

```ts
const index = new VectorizeBinding() // topK: 5 by default

await index.upsert(ctx.env, [{ id: doc.id, values: embedding, metadata: { title: doc.title } }])
const matches = await index.query(ctx.env, embedding)
```

Writes are asynchronous — `insert`, `upsert`, and `deleteByIds` return a mutation id, not the applied state.

## Hyperdrive

```json
"hyperdrive": [{ "binding": "HYPERDRIVE", "id": "<id>" }]
```

```ts
const hyperdrive = new HyperdriveBinding()

const client = new Client({ connectionString: hyperdrive.connectionString(ctx.env) })
```

The connection string carries credentials scoped to the running Worker instance — never log it or return it to a client.

## Durable Objects

```json
"durable_objects": { "bindings": [{ "name": "DO", "class_name": "Counter" }] }
```

```ts
const counters = new DurableObjectBinding()

const stub = counters.stubByName(ctx.env, `room:${roomId}`)
const response = await stub.fetch('https://do/increment')
```

A Durable Object binding also needs the class exported from the Worker entry and a migration entry in `wrangler.json` —
see [Cloudflare's Durable Objects docs](https://developers.cloudflare.com/durable-objects/).

## Analytics Engine

```json
"analytics_engine_datasets": [{ "binding": "TELEMETRY", "dataset": "worker_logs" }]
```

```ts
const analytics = new AnalyticsBinding()

analytics.writeSafe(ctx.env, { indexes: ['/api/v1/articles'], blobs: ['POST', '201'], doubles: [42, 1] })
```

`writeSafe` swallows failures and returns `false` — telemetry should never fail a request. Use `write` when a failed
write should surface.

Most code should reach for [`AnalyticsService`](../../src/core/services/AnalyticsService.ts) rather than this binding,
and should send **metrics only**: logs belong to the observability logstream. See the
[Observability Guide](observability.md).

## Rate limiting

```json
"ratelimits": [{ "name": "RATE_LIMITER", "namespace_id": "1001", "simple": { "limit": 100, "period": 60 } }]
```

```ts
const limits = new RateLimitService()

if (!(await limits.admit(ctx.env, ctx.get('clientIp')))) {
  return ctx.json({ message: 'Too many requests', success: false, data: null }, 429)
}
```

Unlike every other binding here, the interesting settings are not in `config/` and cannot be: `limit` and `period` are
fixed in `wrangler.json` at deploy time, not passed at runtime. Only the binding name is a code-side decision. A Worker
needing two different rates declares two bindings and constructs one accessor per binding — `namespace_id` is what
separates them, and it must be unique within the Worker.

Three things to know before relying on it:

- **Calling `admit` is the increment.** There is no way to ask without counting, so check exactly once per request.
- **Enforcement is per Cloudflare location and eventually consistent.** The effective global rate is higher than the
  configured one, by roughly the number of locations seeing traffic. Treat the number as a floor.
- **It fails open by default.** With no binding declared every request is admitted, so an unconfigured limiter degrades
  to no limit rather than to no service. Set `rateLimit.failOpen` to `false` from `config/` where the limiter is the
  only thing standing between an unauthenticated endpoint and someone else's bill.

That imprecision is fine for shedding abuse and wrong for anything that must balance — never use it as a quota a
customer is billed against.

Where this earns its place is the endpoint that cannot be addressed by an API key: a browser-facing route, or anything
named in `excludedRoutes`. The origin allowlist says _who_ may call; this says _how often_. See the
[`SecurityMiddleware`](../../src/middlewares/SecurityMiddleware.ts).

## Workflows

```json
"workflows": [{ "binding": "WORKFLOW", "name": "data-processing", "class_name": "DataWorkflow" }]
```

```ts
const workflow = new WorkflowBinding<DataRequest>()

const run = await workflow.create(ctx.env, { source: 'upload', objectKey })
const state = await workflow.status(ctx.env, run.id)
```

Like a Durable Object binding, the `wrangler.json` entry names a class the Worker itself exports — here a
`WorkflowEntrypoint` whose `run(event, step)` holds the steps. That class is the Worker's, not this package's: a base
class that must be exported from the consumer's own entry has nothing an adapter can add.

This accessor is the producer side. `create` returns as soon as the instance is accepted, so an endpoint that starts an
hour-long run answers in milliseconds and the caller polls `status` for the outcome. Passing an `id` is the only
idempotency Cloudflare offers — a second start under an existing id throws rather than running twice.

`createBatch` accepts up to 100 instances. Each instance payload is also subject to Cloudflare's current Workflow event
payload limit. Nothing here chunks on the caller's behalf; the right chunk size depends on payload size, which only the
caller knows. See Cloudflare's [Workers API](https://developers.cloudflare.com/workflows/build/workers-api/) and
[Workflow limits](https://developers.cloudflare.com/workflows/reference/limits/).

`successRetention` and `errorRetention` in `config/` bound how long finished instances keep their output. A call that
names `retention` itself owns both halves of it rather than inheriting one from the configuration.

## Browser Rendering

```json
"browser": { "binding": "BROWSER" }
```

```ts
const browser = new BrowserBinding()

if (!browser.isBound(ctx.env)) {
  return ctx.json({ message: 'This deployment cannot render pages', success: false, data: null }, 503)
}

const session = await puppeteer.launch(browser.resolve(ctx.env))
```

The only accessor here that carries no methods of its own, and the only one whose binding is typed as `Fetcher` — which
is what the runtime hands over, and is structurally what `@cloudflare/puppeteer` asks for, so the resolved binding
passes straight in without a cast. Driving the browser is the consumer's job and its dependency; what this buys is a
`MissingBindingError` naming `BROWSER` instead of `undefined` failing somewhere inside a browser driver.

## Flagship

```json
"flagship": [{ "binding": "FLAGS", "app_id": "<app id>" }]
```

```ts
const flags = new FlagshipBinding()

// The second argument is the deployed behaviour, and the answer if Flagship is unreachable.
const channel = await flags.string(ctx.env, 'release-channel', ctx.env.RELEASE_CHANNEL ?? 'stable')
const enabled = await flags.boolean(ctx.env, 'new-checkout', false, { cohort })
```

**The one accessor here that does not throw on a missing binding**, and the exception is deliberate. Everywhere else a
missing binding means the request cannot be served and should say so. A flag decides between two paths that both work,
so a Worker that cannot reach Flagship should take the path it took before flags existed. `boolean`, `string`, `number`
and `object` therefore answer with their fallback when the binding is absent, when evaluation fails, and when the flag's
type does not match — an evaluation error is logged and swallowed, the way `AnalyticsBinding.writeSafe` treats a failed
telemetry write.

Which makes the fallback the most important argument in the call. It is the deployed behaviour, not boilerplate: pass
what the Worker used before the flag existed — for a `vars` entry being migrated, that entry. `resolve(env)` is still
inherited and still throws, for a caller that genuinely cannot proceed without a live evaluation.

The `app_id` lives in `wrangler.json` rather than in settings, so a Worker reading two Flagship apps declares two
bindings and constructs one accessor per binding. `context` in `config/` is merged into every evaluation — the
deployment-wide half of a targeting rule, such as the service name or the environment — and a call's own context wins on
any key both name.

Only the four value methods are exposed. The platform's `*Details` variants carry `variant` and `reason` for experiment
analysis, which nothing here does yet; the untyped `get` returns `unknown`, which the typed methods strictly improve on.
Both remain reachable through `resolve(env)`.

## AI Search

Two bindings, and which one to declare is the whole decision.

**One corpus, fixed at deploy time:**

```json
"ai_search": [{ "binding": "AI_SEARCH", "instance_name": "documentation" }]
```

```ts
const documentation = new AiSearchBinding()

const { chunks } = await documentation.search(ctx.env, { query: 'deployment configuration' })
const answer = await documentation.chat(ctx.env, { messages })
```

**A corpus per tenant, created as tenants arrive:**

```json
"ai_search_namespaces": [{ "binding": "AI_SEARCH_NAMESPACE", "namespace": "tenants" }]
```

```ts
const corpora = new AiSearchNamespaceBinding()

const tenant = corpora.instance(ctx.env, `tenant-${id}`)
const { chunks } = await tenant.search({ query })
```

`instance_name` must exist at deploy time, which is exactly what the namespace binding removes — there, a request names
the instance, so a corpus created when a customer signs up is reachable without a deploy. The default binding names
differ (`AI_SEARCH` and `AI_SEARCH_NAMESPACE`) because a Worker may declare both, and two bindings cannot share a name
on one `env`.

`instance()` is synchronous and local — it names an instance rather than fetching it, so one that does not exist fails
on the first operation instead. It hands back the platform's own `AiSearchInstance`, so `items`, `jobs` and the rest
stay reachable without this package in the way.

`search` retrieves passages and generates nothing; `chat` does both and returns the `chunks` its answer came from, which
is what makes a citation possible. Streaming is `chatStream` rather than `stream: true` on `chat`, because the two
return different things and a boolean that changes a return type is a discriminated union pretending to be an option.

A namespace `search` fans out and **reports partial failure rather than hiding it**: chunks come back tagged with their
instance, and `errors` names the instances that did not answer. Ignoring `errors` means reading a result set that
silently lost a corpus.

`options` in `config/` supplies retrieval defaults — result count, score floor, reranking — all-or-nothing, like
Workflows' retention: a request naming `ai_search_options` owns the whole object, since deep-merging four nested
sub-objects would give a call site an effective configuration it could not predict from reading itself.

### Not `env.AI.autorag()`

The older path to this product hung off the Workers AI binding, as `env.AI.autorag(id)` and `env.AI.aiSearch()`. Both
are deprecated in the `@cloudflare/workers-types` version this package pins, in favour of the standalone bindings above.
`AIBinding` therefore does not expose them — routing AI Search through it would have adopted a deprecated surface on the
day it was written.

### Against Vectorize

Both do retrieval, and the difference is who owns the pipeline. With [Vectorize](#vectorize) a Worker owns the embedding
model, the chunking, and the index lifecycle; with AI Search it owns none of them and asks in words instead of vectors.
Neither replaces the other — the question is whether running an embedding pipeline is work you want.

## Workers VPC

Two bindings, and choosing between them is a security decision before it is an ergonomic one.

**One private service, fixed at deploy time:**

```json
"vpc_services": [{ "binding": "VPC_SERVICE", "service_id": "<service id>", "remote": true }]
```

```ts
const internal = new VpcServiceBinding()

const response = await internal.fetch(ctx.env, 'https://internal-api.example.com/data')
```

**A whole private network, destination chosen per call:**

```json
"vpc_networks": [{ "binding": "VPC_NETWORK", "network_id": "cf1:network", "remote": true }]
```

```ts
const network = new VpcNetworkBinding()

const response = await network.fetch(ctx.env, 'http://10.0.1.50:8080/api/data')
const socket = await network.connect(ctx.env, '10.0.1.50:6379')
```

Both reach services on a private network — an internal API, a database, something in an AWS VPC or on a rack — through a
Cloudflare Tunnel, so the private side needs no inbound firewall rule and nothing is exposed to the internet.

**Prefer a VPC Service wherever one endpoint will do.** Its `service_id` fixes the destination at deploy time, so a
Worker holding it can reach exactly one private endpoint and no request it handles can talk it into reaching another. A
VPC Network trades that away: any address on the bound tunnel or mesh is in range, which means an address derived from
an incoming request is a server-side request forgery with your private network behind it. Cloudflare's own answer to
constraining that is a VPC Service per destination rather than a filter, and this package offers no allowlist setting
for the same reason — a security control enforced in library code is one a caller routes around by calling
`resolve(env)`.

`connect()` is plaintext TCP at the time of writing, for the services that are not HTTP — Redis, Memcached, MQTT. A
private service expecting TLS needs it terminated on the private side. The socket's lifetime belongs to the caller;
nothing here wraps it, because closing and draining it depends on the protocol being spoken and this package knows none.

### The one type this package declares itself

`VpcServiceBinding` is `Binding<Fetcher>` — a VPC Service binding offers `fetch` alone, which is `Fetcher` structurally,
the same reasoning that types [Browser Rendering](#browser-rendering).

`VpcNetworkBinding` could not do that, because `connect` has no existing type to borrow. **Workers VPC is in beta and
`@cloudflare/workers-types` declares nothing for it at the version this package pins**, so `VpcNetwork` is written here
— the only binding shape in the package not taken from Cloudflare's own types. It is kept to the two documented methods
deliberately: a hand-written type cannot be checked against the runtime, so a smaller invented surface is a smaller
thing to be wrong about. When the real type ships, that file should be deleted rather than maintained.

## Adding a binding of your own

1. Declare it in `wrangler.json`.
2. Add the field to the `Env` interface in [`src/types/CloudflareEnv.ts`](../../src/types/CloudflareEnv.ts).
3. Declare its settings in [`src/types/`](../../src/types/) — an interface extending `BindingSettings`, plus an entry on
   `CloudflareConfiguration` — and give it defaults in [`src/constants/Defaults.ts`](../../src/constants/Defaults.ts).
   It is overridable from a Worker's `config/` from this point on; see
   [Register the configuration surface](../getting-started/quick-start.md#2-register-the-configuration-surface).
4. Add a `*Binding` to `src/core/bindings/` extending `Binding<T>`, taking its settings through `lazySettings` and
   exposing only the operations your code needs:

```ts
import { Binding } from '@/core/bindings/Binding'
import { lazySettings } from '@/utils/lazySettings'

import type { EmailSettings } from '@/types/EmailSettings'

export class EmailBinding extends Binding<SendEmail> {
  constructor(settings?: EmailSettings) {
    const settle = lazySettings<EmailSettings>('email', settings)

    // A thunk, never a value: resolving here would throw on import.
    super(() => settle().binding)
  }

  public async send(env: Env, message: EmailMessage): Promise<void> {
    await this.resolve(env).send(message)
  }
}
```

5. Re-export it from [`src/core/bindings/index.ts`](../../src/core/bindings/index.ts).
6. If it deserves a worked example, add a `*Service` beside the others in
   [`src/core/services/`](../../src/core/services/) and re-export it from that directory's barrel. Anything with
   business meaning belongs in the consuming Worker's own `services/` instead.
7. If the capability is one another platform could also provide, declare an interface for it in the kernel's
   `types/capabilities/` and have the service `implement` it. Take the runtime handle as a type parameter —
   `interface MailSender<TRuntime>` — so the kernel stays free of `@cloudflare/workers-types`, and state in the
   interface whether an unavailable implementation degrades or throws. Skip this step for something genuinely
   Cloudflare-shaped; an interface with one possible implementation is ceremony, not abstraction.
