Skip to main content

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.

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

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

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.
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).
function meterUsage(
  env: MeteringEnv,
  kind: string,
  fields: { id?: string; op?: string; units?: number; count?: number },
): boolean
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.
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.
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.
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.
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.

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.

Manifest validation

For tooling that introspects or generates binding manifests.
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, INTERNAL_STORAGE_HMAC_SECRET, 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 ValidationErrors. The three name/type sets are exported as Set<string> instances, not tuples - use .has(...) rather than indexing.

CustomBinding (wire type)

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