Skip to content

Instantly share code, notes, and snippets.

@dmmulroy
Created January 4, 2026 01:44
Show Gist options
  • Select an option

  • Save dmmulroy/38e516ec334174ddd14dc54d66a4c0a7 to your computer and use it in GitHub Desktop.

Select an option

Save dmmulroy/38e516ec334174ddd14dc54d66a4c0a7 to your computer and use it in GitHub Desktop.

Worker Bindings Architecture

1. How env.MY_D1 Works Under the Hood

Data Flow: Configuration → C++ → JavaScript

workerd.capnp config → server.c++ (parsing) → WorkerdApi::Global → createBindingValue() → v8::Object (env)

2. Key Files and Abstractions

A. Configuration Schema (Cap'n Proto)

File: src/workerd/server/workerd.capnp

  • Lines 335-474: struct Binding - defines all binding types
  • Lines 578-596: struct WrappedBinding - for composed bindings like D1
struct Binding {
  name @0 :Text;
  union {
    text @4 :Text;
    service @9 :ServiceDesignator;
    kvNamespace @11 :ServiceDesignator;
    wrapped @14 :WrappedBinding;
    # ... other binding types
  }
}

struct WrappedBinding {
  moduleName @0 :Text;      # e.g., "cloudflare-internal:d1-api"
  entrypoint @1 :Text = "default";
  innerBindings @2 :List(Binding);  # e.g., fetcher service binding
}

B. Config Parsing → Global Struct

File: src/workerd/server/server.c++

  • Lines 3437-3683: createBinding() - converts capnp config to WorkerdApi::Global
// server.c++ line 3664-3682
case config::Worker::Binding::WRAPPED: {
  auto wrapped = binding.getWrapped();
  kj::Vector<Global> innerGlobals;
  for (const auto& innerBinding: wrapped.getInnerBindings()) {
    KJ_IF_SOME(global, createBinding(...)) {
      innerGlobals.add(kj::mv(global));
    }
  }
  return makeGlobal(Global::Wrapped{
    .moduleName = kj::str(wrapped.getModuleName()),
    .entrypoint = kj::str(wrapped.getEntrypoint()),
    .innerBindings = innerGlobals.releaseAsArray(),
  });
}

C. Global Struct Definition

File: src/workerd/server/workerd-api.h

  • Lines 85-280: struct Global - intermediate representation of bindings
struct Global {
  kj::String name;
  kj::OneOf<
    Json,
    Fetcher,           // service bindings (channel-based)
    KvNamespace,       // KV (channel + bindingName)
    R2Bucket,          // R2 (channel + bucket + bindingName)
    Wrapped,           // D1, etc. (moduleName + entrypoint + innerBindings)
    DurableActorNamespace,
    // ... more types
  > value;
};

D. V8 Value Creation

File: src/workerd/server/workerd-api.c++

  • Lines 645-841: createBindingValue() - converts Globalv8::Value
// workerd-api.c++ line 780-810 (Wrapped binding instantiation)
KJ_CASE_ONEOF(wrapped, Global::Wrapped) {
  auto moduleRegistry = jsg::ModuleRegistry::from(lock);
  auto moduleName = kj::Path::parse(wrapped.moduleName);  // "cloudflare-internal:d1-api"

  KJ_IF_SOME(moduleInfo, moduleRegistry->resolve(lock, moduleName, ...)) {
    auto module = moduleInfo.module.getHandle(lock);
    jsg::instantiateModule(lock, module);

    // Build env object with inner bindings (e.g., {fetcher: Fetcher})
    auto env = v8::Object::New(lock.v8Isolate);
    for (const auto& innerBinding: wrapped.innerBindings) {
      lock.v8Set(env, innerBinding.name,
          createBindingValue(lock, innerBinding, ...));  // Recursive!
    }

    // Call makeBinding(env) → D1Database instance
    auto fn = lock.v8Get(moduleNs, wrapped.entrypoint);  // "default"
    value = fn->Call(context, context->Global(), 1, &env);
  }
}

E. Worker Constructor - Populating env Object

File: src/workerd/io/worker.c++

  • Lines 1704-1928: Worker::Worker() constructor
// worker.c++ line 1800-1818
if (useModuleSyntax) {
  bindingsScope = v8::Object::New(lock.v8Isolate);  // Creates 'env'
} else {
  bindingsScope = context->Global();  // Service worker syntax: globals
}

// Load all globals/bindings
for (auto& global: script->impl->globals) {
  lock.v8Set(bindingsScope, global.name, global.value);
}

compileBindings(lock, api, bindingsScope, ctxExports);  // Calls compileGlobals

// For ES modules, attach env to handler
obj.env = lock.v8Ref(bindingsScope.As<v8::Value>());

F. JSG (JavaScript Glue) Layer

File: src/workerd/jsg/jsg.h

  • Line 56-82: JSG_RESOURCE_TYPE macro
// Example: KV binding class (kv.h line 22-276)
class KvNamespace: public jsg::Object {
public:
  explicit KvNamespace(kj::String bindingName,
                       kj::Array<AdditionalHeader> additionalHeaders,
                       uint subrequestChannel)
      : additionalHeaders(kj::mv(additionalHeaders)),
        subrequestChannel(subrequestChannel),
        bindingName(kj::mv(bindingName)) {}

  jsg::Promise<GetResult> get(jsg::Lock& js, kj::String name, ...);

  JSG_RESOURCE_TYPE(KvNamespace, CompatibilityFlags::Reader flags) {
    JSG_METHOD(get);
    JSG_METHOD(list);
    JSG_METHOD(put);
    // ...
  }

private:
  uint subrequestChannel;  // Channel ID for IoContext::getSubrequestChannel()
};

3. D1 Concrete Example

D1 uses the "Wrapped Binding" pattern:

Step 1: Config

d1-api-test.wd-test line 14-24:

bindings = [(
  name = "d1",
  wrapped = (
    moduleName = "cloudflare-internal:d1-api",
    innerBindings = [(
      name = "fetcher",
      service = "d1-mock"
    )],
  )
)]

Step 2: TypeScript Module

src/cloudflare/internal/d1-api.ts:

// Line 811-813
export default function makeBinding(env: { fetcher: Fetcher }): D1Database {
  return new D1Database(env.fetcher);
}

// D1Database uses fetcher.fetch() for all operations
class D1Database {
  constructor(fetcher: Fetcher) {
    this.fetcher = fetcher;
  }

  // All operations go through HTTP to the d1 service
  async _send(endpoint: string, ...) {
    const response = await this._wrappedFetch(url.href, {...});
    // ...
  }
}

Step 3: Module Registration

src/workerd/api/modules.h line 93:

registry.addBuiltinBundle(CLOUDFLARE_BUNDLE);  // Contains d1-api.js

Step 4: Bundle Generation

src/cloudflare/BUILD.bazel:

wd_ts_bundle(
    name = "cloudflare",
    import_name = "cloudflare",
    internal_modules = glob(["internal/*.ts"]),  # Includes d1-api.ts
    # ...
)

4. Channel-Based I/O Architecture

File: src/workerd/io/io-context.h

  • Lines 835-888: Channel-based subrequest system
// Each binding gets a numeric "channel" that maps to a service
kj::Own<WorkerInterface> getSubrequestChannel(
    uint channel,           // Index into IoChannelFactory
    bool isInHouse,         // KV/R2 = true, user services = false
    kj::Maybe<kj::String> cfBlobJson,
    kj::ConstString operationName);

Channel Assignment (server.c++ line 3630-3637):

case config::Worker::Binding::KV_NAMESPACE: {
  uint channel = static_cast<uint>(subrequestChannels.size()) +
      IoContext::SPECIAL_SUBREQUEST_CHANNEL_COUNT;  // Starts at 2
  subrequestChannels.add(FutureSubrequestChannel{binding.getKvNamespace(), ...});
  return makeGlobal(Global::KvNamespace{
    .subrequestChannel = channel, .bindingName = kj::str(binding.getName())});
}

5. Architecture Diagram

┌─────────────────────────────────────────────────────────────────────────────┐
│                           Configuration Layer                                │
├─────────────────────────────────────────────────────────────────────────────┤
│ workerd.capnp:                                                               │
│   bindings = [(name = "MY_D1", wrapped = (moduleName = "...:d1-api", ...))] │
└───────────────────────────────────┬─────────────────────────────────────────┘
                                    │ parsed by
                                    ▼
┌─────────────────────────────────────────────────────────────────────────────┐
│                           C++ Binding Creation                               │
├─────────────────────────────────────────────────────────────────────────────┤
│ server.c++: createBinding() → WorkerdApi::Global struct                     │
│ workerd-api.c++: createBindingValue() → v8::Local<v8::Value>                │
│                                                                              │
│ For wrapped bindings:                                                        │
│   1. Resolve module from ModuleRegistry                                      │
│   2. Create inner bindings (e.g., Fetcher with channel)                     │
│   3. Call module's makeBinding(env) → returns JS object                     │
└───────────────────────────────────┬─────────────────────────────────────────┘
                                    │ attached to
                                    ▼
┌─────────────────────────────────────────────────────────────────────────────┐
│                           Worker Initialization                              │
├─────────────────────────────────────────────────────────────────────────────┤
│ worker.c++: Worker constructor                                               │
│   - Creates v8::Object for 'env'                                             │
│   - Populates with all binding values                                        │
│   - Attaches to ExportedHandler.env                                          │
└───────────────────────────────────┬─────────────────────────────────────────┘
                                    │ exposed to
                                    ▼
┌─────────────────────────────────────────────────────────────────────────────┐
│                           JavaScript Runtime                                 │
├─────────────────────────────────────────────────────────────────────────────┤
│ export default { fetch(request, env) { ... } }                              │
│                                                                              │
│ env.MY_D1 → D1Database instance                                             │
│   └── this.fetcher → Fetcher (JSG_RESOURCE_TYPE)                            │
│       └── fetch() → IoContext::getSubrequestChannel(channel)                │
│           └── IoChannelFactory::startSubrequest()                           │
│               └── WorkerInterface → HTTP to D1 service                      │
└─────────────────────────────────────────────────────────────────────────────┘

6. Key Abstractions Summary

Abstraction File Purpose
Binding (capnp) workerd.capnp:335 Configuration schema
WorkerdApi::Global workerd-api.h:85 C++ intermediate representation
createBinding() server.c++:3437 Config → Global conversion
createBindingValue() workerd-api.c++:645 Global → v8::Value
JSG_RESOURCE_TYPE jsg.h:56 C++ class → JS object binding
IoChannelFactory io-channels.h:97 I/O channel abstraction
subrequestChannel throughout Numeric channel for service routing
Wrapped binding d1-api.ts TS module wrapping inner bindings

7. Binding Type Patterns

Native C++ Bindings (KV, R2, Durable Objects)

Direct C++ class with JSG_RESOURCE_TYPE:

// Created in createBindingValue()
value = lock.wrap(context,
    lock.alloc<api::KvNamespace>(kj::str(ns.bindingName), ..., ns.subrequestChannel));

Wrapped Bindings (D1, Hyperdrive, etc.)

TypeScript module wrapping inner bindings:

// cloudflare-internal:d1-api
export default function makeBinding(env: { fetcher: Fetcher }): D1Database {
  return new D1Database(env.fetcher);
}

Service Bindings

Simple Fetcher with channel:

return makeGlobal(Global::Fetcher{
  .channel = channel, .requiresHost = true, .isInHouse = false});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment