Sandbox & runtime

Learn how extension code executes including what's allowed, what's stripped, and what's enforced by the sandbox.

Extension scripts run in a JavaScript sandbox. Each invocation gets a fresh evaluation context with the standard library injected and the manifest’s permissions enforced.

What’s allowed

  • Plain JS/TS — async/await, classes, generators, regex, etc.
  • The standard library: Entity, Log, Http, Settings, Secrets, api, plus per-entity classes (Invoice, Customer, etc) and any custom entities you declare. All exported from @backfill-io/sdk, and also exposed as globals at runtime.
  • SDK imports from @backfill-io/sdk and subpaths like @backfill-io/sdk/ui.
  • Relative helpers and browser-compatible npm packages that can be bundled at build time.

What’s stripped at deploy

The CLI bundles each executable entrypoint before upload. Relative helpers and pure JavaScript package dependencies are inlined into that entrypoint bundle. @backfill-io/sdk imports are left external, then stripped by the server before execution. At runtime, the SDK identifiers live as globals, not as imports.

import { Log } from "@backfill-io/sdk";   // edit-time only
Log.info("hello");                     // runtime: Log is a global

What’s rejected

Imports that cannot be bundled or cannot run in the sandbox fail during backfill build:

src/hooks/invoice/after-save.ts: Unsupported import "../../assets/logo.svg" —
v1 extension bundles support .ts, .tsx, .js, and .json modules only

So no CSS, SVG, images, node:fs, node:crypto, native addons, dynamic require, dynamic import, or packages that depend on Node process-level globals.

What’s gated by permissions

backfill.config.ts declares three permission categories. Calls that exceed them are blocked at runtime.

permissions: {
  entities: { Invoice: ["read", "write"], Customer: ["read"] },
  http: ["https://api.stripe.com/*"],
  secrets: { stripe_api_key: ["read"] },
}
  • Entities: "read" and "write". Other verbs rejected at deploy.
  • HTTP: each entry must be https:// with an explicit host. Path wildcards yes; host wildcards no.
  • Secrets: names match ^[a-z][a-z0-9_]{0,62}$.

Observation mode

backfill deploy --observe

Deploys a version that runs against live traffic without committing writes. Hooks fire. Log calls flow normally. Entity.create and Entity.update are intercepted and recorded but not persisted. Use this to validate a new version before you cut over:

backfill deploy --observe
backfill logs --follow
# Satisfied? Promote it
backfill promote

Local validation

backfill build runs the same manifest extraction, file-type discovery, hook event whitelist, route auth check, entity field-type check, and import validator the server runs. You see all the same errors locally before anything goes near the network.