> ## Documentation Index
> Fetch the complete documentation index at: https://docs.deep.space/llms.txt
> Use this file to discover all available pages before exploring further.

# Bindings reference

> Migration runner, per-tenant metering, and binding manifest exports.

```ts theme={null}
import {
  runMigrations,
  meterAi, meterVectorize, meterUsage,
  COST_RATES,
  AUTO_PROVISION_SENTINEL, AUTO_PROVISIONABLE_TYPES,
  ALLOWED_BINDING_TYPES, RESERVED_BINDING_NAMES,
  validateBindingManifest, isAutoProvision,
  bindingManifestFromOutputConfig,
} from 'deepspace/worker'

import { captureScreenshot } from 'deepspace/server'

import type {
  CustomBinding, CustomBindingManifest, ValidationError,
  RunMigrationsResult,
} from 'deepspace/worker'

import type {
  ScreenshotEnv, ScreenshotOptions, ScreenshotResult,
} from 'deepspace/server'
```

## `runMigrations(db, migrations)`

Idempotently apply a list of SQL migrations to a D1 database. Designed to run at worker startup.

```ts theme={null}
function runMigrations(
  db: D1Database,
  migrations: readonly string[],
): Promise<RunMigrationsResult>

interface RunMigrationsResult {
  fromVersion: number
  toVersion: number
  applied: number
}
```

State is tracked in a meta-table `_dpc_migrations(idx INTEGER PRIMARY KEY, applied_at TEXT NOT NULL)`.

**Contract:**

* Each array entry is one migration; index in the array is its sequence number.
* Each migration string can contain multiple `;`-separated statements.
* **Don't put `;` inside string literals** - the split is naive.
* Statements run via `db.prepare(sql).run()`, not `exec()`.
* Idempotent - re-running with the same array is a no-op.
* **Append new migrations to the end; never reorder or delete entries.**
* Throws on any individual migration failure; the failed row is not inserted, so the next deploy retries.

```ts theme={null}
await runMigrations(env.MY_DB, [
  `CREATE TABLE notes (
     id TEXT PRIMARY KEY,
     body TEXT NOT NULL,
     created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
   );
   CREATE INDEX idx_notes_created ON notes(created_at);`,
  `ALTER TABLE notes ADD COLUMN tags TEXT;`,
])
```

## Per-tenant metering

Every deployed app gets a `USAGE_EVENTS` Analytics Engine binding automatically - don't declare it in `wrangler.toml`. The metering helpers write usage events keyed by `OWNER_USER_ID`.

All three helpers take a `MeteringEnv` shape - `{ USAGE_EVENTS?, OWNER_USER_ID?, APP_NAME? }` - which the app's `Env` already satisfies because the deploy worker injects all three. The runtime `MeteringEnv` is intentionally weaker than `Env` so the helpers compile in shared utility code.

### `meterAi(env, model, fields)`

```ts theme={null}
function meterAi(
  env: MeteringEnv,
  model: string,
  fields: { inputChars?: number; outputChars?: number; calls?: number },
): boolean
```

Emits `op='input'` and `op='output'` events. If both are 0, emits `op='call'` so the model invocation is still recorded.

```ts theme={null}
const result = await env.AI.run('@cf/meta/llama-3.1-8b', { messages })
meterAi(env, '@cf/meta/llama-3.1-8b', {
  inputChars: JSON.stringify(messages).length,
  outputChars: result.response?.length ?? 0,
})
```

### `meterVectorize(env, indexName, op, fields)`

```ts theme={null}
function meterVectorize(
  env: MeteringEnv,
  indexName: string,
  op: 'query' | 'upsert' | 'delete' | 'getByIds',
  fields: { vectors?: number; dims?: number; storedCount?: number },
): boolean
```

Units calculation:

* **`query`**: `(vectors + storedCount) * dims` (matches CF's `(stored + queries) * dims` formula)
* **`upsert` / `delete` / `getByIds`**: `vectors * dims`

Pass `storedCount` on queries against non-empty indexes or you'll significantly undercount.

```ts theme={null}
const matches = await env.VEC.query(embedding, { topK: 10 })
meterVectorize(env, 'docs', 'query', {
  vectors: 1,
  dims: 768,
  storedCount: await env.VEC.describe().then(d => d.vectorsCount),
})
```

### `meterUsage(env, kind, fields)`

Generic fallback for any other binding (Browser Rendering, Hyperdrive, custom kinds).

```ts theme={null}
function meterUsage(
  env: MeteringEnv,
  kind: string,
  fields: { id?: string; op?: string; units?: number; count?: number },
): boolean
```

```ts theme={null}
const pdf = await env.BROWSER.fetch(url).then(r => r.blob())
meterUsage(env, 'browser', { id: 'render', units: 1, count: 1 })
```

### Behavior

All three helpers return `boolean` - `false` when `USAGE_EVENTS` is missing (local dev) or when Analytics Engine throws. **Metering never breaks the calling code path** - wrap in `void` if you prefer to ignore the return.

### `COST_RATES`

Per-`units` USD multipliers for dashboard rollup. Multiply `SUM(_sample_interval * doubles[1])` by the matching rate to get USD without re-querying Cloudflare's billing API.

```ts theme={null}
const COST_RATES = {
  ai: {
    /** USD per character (input or output). */
    perChar: number,
  },
  vectorize: {
    /** USD per queried dimension. */
    queriedPerDim: number,
    /** USD per stored dimension per month. */
    storedPerDimPerMonth: number,
  },
} as const
```

The shape is a nested object grouped by binding kind, not a flat record of dotted keys.

## Shared Browser Rendering (`captureScreenshot`)

Render a URL to a PNG via the platform's shared Browser Rendering binding, without declaring your own `[browser]` binding in `wrangler.toml`. Added in v0.3.10.

```ts theme={null}
function captureScreenshot(
  env: ScreenshotEnv,
  opts: ScreenshotOptions,
): Promise<ScreenshotResult | null>

interface ScreenshotOptions {
  url: string
  viewport?: { width: number; height: number }
  waitUntil?: 'load' | 'domcontentloaded' | 'networkidle0' | 'networkidle2'
  timeoutMs?: number
  fullPage?: boolean
}

interface ScreenshotEnv extends PlatformWorkerEnv {
  APP_NAME: string
  APP_IDENTITY_TOKEN: string
}

interface ScreenshotResult {
  /** PNG bytes. */
  body: ArrayBuffer
  /** `image/png`. */
  contentType: string
}
```

The helper POSTs to the platform-worker's internal screenshot endpoint, signed with `APP_IDENTITY_TOKEN` and `APP_NAME` (the same HMAC-of-app-name pattern `/internal/files` uses). The platform enforces a host allowlist (`*.app.space`, `*.deep.space`), per-app sliding rate limits, and viewport / timeout clamping.

Imported from `'deepspace/server'` - this is a server-side helper, not a browser export.

### Example: OG-image route

The scaffold's `worker.ts` already binds `APP_IDENTITY_TOKEN`, `APP_NAME`, and the `PLATFORM_WORKER` service binding, so the route below is copy-paste-runnable - no `wrangler.toml` edits required.

```ts theme={null}
import { captureScreenshot } from 'deepspace/server'

app.get('/api/og/:roomId', async (c) => {
  const roomId = c.req.param('roomId')
  const shot = await captureScreenshot(c.env, {
    url: `https://${c.env.APP_NAME}.app.space/rooms/${roomId}/preview`,
    viewport: { width: 1200, height: 630 },
    waitUntil: 'networkidle0',
    timeoutMs: 8000,
  })

  if (!shot) {
    // Allowlist miss, rate limit, timeout, or local dev without a token.
    // Redirect to a static placeholder so the caller still gets an image.
    return c.redirect('/og-placeholder.png', 302)
  }

  return new Response(shot.body, {
    headers: {
      'content-type': shot.contentType,
      'cache-control': 'public, max-age=86400',
    },
  })
})
```

### The `null` return is the contract

`captureScreenshot` returns `null` on **any** non-2xx from the platform - rate limit, allowlist miss, target timeout, BR binding misconfigured platform-side. Underlying errors are logged on the platform side; the caller just sees `null`. Always branch on it and fall back to a placeholder. Users will hit this path during local dev (see below) and on the first request after an allowlist change.

<Info>
  `APP_IDENTITY_TOKEN` is only populated after your first `npx deepspace deploy`. Calls before that, or from a fresh `deepspace dev` session with no prior deploy, will return `null`. Test screenshot flows against a deployed environment, or guard with a placeholder during local bring-up.
</Info>

### When you still want your own `browser_rendering` binding

`captureScreenshot` covers standard preview / OG-image flows on app-owned hosts. Declare your own `[browser]` binding in `wrangler.toml` only if you need one of:

* **Unmetered Browser Rendering** - the shared binding counts against the platform's per-app sliding rate limit; an app-owned binding is billed and rate-limited on your CF account.
* **Custom user agents, headers, or cookies** - the shared endpoint sets these platform-side and does not accept overrides.
* **Third-party hosts** - URLs outside `*.app.space` / `*.deep.space` are rejected by the allowlist. Render them via your own binding.
* **Direct Puppeteer scripting** (`page.evaluate`, multi-step navigation, PDF export) - the shared endpoint exposes a single capture call only.

For those cases, add `[browser] binding = "BROWSER"` to `wrangler.toml` and call `env.BROWSER.fetch(...)` directly - see [custom bindings](/guides/custom-bindings).

## Manifest validation

For tooling that introspects or generates binding manifests.

```ts theme={null}
const AUTO_PROVISION_SENTINEL: 'auto'

const AUTO_PROVISIONABLE_TYPES: Set<string>
// 'd1', 'kv_namespace', 'vectorize', 'r2_bucket', 'queue'

const ALLOWED_BINDING_TYPES: Set<string>
// 'vectorize', 'ai', 'r2_bucket', 'kv_namespace',
// 'd1', 'queue', 'browser_rendering', 'analytics_engine', 'hyperdrive'

const RESERVED_BINDING_NAMES: Set<string>
// ASSETS, PLATFORM_WORKER, API_WORKER, APP_NAME, OWNER_USER_ID,
// AUTH_JWT_PUBLIC_KEY, AUTH_JWT_ISSUER, AUTH_WORKER_URL,
// APP_IDENTITY_TOKEN, APP_OWNER_JWT, USAGE_EVENTS

function validateBindingManifest(
  manifest: unknown,
):
  | { valid: true; bindings: CustomBindingManifest }
  | { valid: false; errors: ValidationError[] }

function isAutoProvision(b: CustomBinding): boolean

function bindingManifestFromOutputConfig(
  outputConfig: Record<string, unknown>,
): CustomBindingManifest

interface ValidationError {
  /** Undefined for top-level shape failures (e.g. manifest is not an array). */
  binding?: CustomBinding
  reason: string
}
```

`validateBindingManifest` returns a discriminated union: on success, `{ valid: true, bindings }`; on failure, `{ valid: false, errors }` carrying one or more `ValidationError`s.

The three name/type sets are exported as `Set<string>` instances, not tuples - use `.has(...)` rather than indexing.

## `CustomBinding` (wire type)

```ts theme={null}
type CustomBinding =
  | {
      type: 'vectorize'
      name: string
      /** Either a pre-existing index name or the literal `"auto"`. */
      index_name: string
      /** Required when `index_name === "auto"`. */
      dimensions?: number
      /** Required when `index_name === "auto"`. */
      metric?: 'cosine' | 'euclidean' | 'dot-product'
    }
  | { type: 'ai'; name: string }
  | {
      type: 'r2_bucket'
      name: string
      /** Either a pre-existing bucket name or the literal `"auto"`. */
      bucket_name: string
    }
  | {
      type: 'kv_namespace'
      name: string
      /** Either a pre-existing KV namespace ID or the literal `"auto"`. */
      namespace_id: string
      /** Required when `namespace_id === "auto"`. Human-readable title. */
      title?: string
    }
  | {
      type: 'd1'
      name: string
      /** Either a pre-existing D1 database UUID or the literal `"auto"`. */
      id: string
      /** Required when `id === "auto"`. Human-readable database name. */
      database_name?: string
    }
  | {
      type: 'queue'
      name: string
      /** Either a pre-existing queue name or the literal `"auto"`. */
      queue_name: string
    }
  | { type: 'browser_rendering'; name: string }
  | { type: 'analytics_engine'; name: string; dataset?: string }
  | { type: 'hyperdrive'; name: string; id: string }

type CustomBindingManifest = CustomBinding[]
```

The deploy worker validates inbound manifests against this shape before forwarding to Workers for Platforms. The exact ID field varies by binding kind - `kv_namespace` uses `namespace_id`, `d1` uses `id`, `queue` uses `queue_name`, `r2_bucket` uses `bucket_name`, and `vectorize` uses `index_name`. `analytics_engine.dataset` is optional.

## See also

* [Custom bindings guide](/guides/custom-bindings) - declaring resources in `wrangler.toml`
* [Deployment concepts](/concepts/deployment) - auto-provisioning lifecycle
