bknd logo
Runtimes

Cloudflare

Run bknd inside Cloudflare Worker

Installation

To get started with Cloudflare Workers and bknd you can either install the package manually, and follow the descriptions below, or use the CLI starter:

CLI Starter

Create a new Cloudflare CLI starter project by running the following command:

npx bknd create -i cloudflare

Manual

Create a new cloudflare worker project by following the official guide, and then install bknd as a dependency:

npm install bknd
pnpm install bknd
yarn add bknd
bun add bknd

Serve the API

If you don't choose anything specific, it uses the first D1 binding it finds.

src/index.ts
import { serve, d1 } from "bknd/adapter/cloudflare";

// scans your environment for the first D1 binding it finds
export default serve();

// manually specifying a D1 binding:
export default serve<Env>({
  app: ({ env }) => d1({ binding: env.D1_BINDING }),
});

// or specify binding using `bindings`
export default serve<Env>({
  bindings: ({ env }) => ({ db: env.D1_BINDING }),
});

// or use LibSQL
export default serve<Env>({
  app: ({ env }) => ({ url: env.DB_URL }),
});

For more information about the connection object when using LibSQL, refer to the Database guide.

Now run the worker:

wrangler dev

And confirm it works by opening http://localhost:8787 in your browser.

Serve the Admin UI

Now in order to also server the static admin files, you have to modify the wrangler.toml to include the static assets. You can do so by either serving the static using the new Assets feature, or the deprecated Workers Site.

Assets

Make sure your assets point to the static assets included in the bknd package:

wrangler.toml
assets = { directory = "node_modules/bknd/dist/static" }

Workers Sites

Make sure your site points to the static assets included in the bknd package:

wrangler.toml
[site]
bucket = "node_modules/bknd/dist/static"

And then modify the worker entry as follows:

src/index.ts
import { serve } from "bknd/adapter/cloudflare";
import manifest from "__STATIC_CONTENT_MANIFEST"; 

export default serve<Env>({
  app: () => ({
    /* ... */
  }),
  manifest, 
});

Adding custom routes

You can also add custom routes by defining them after the app has been built, like so:

import { serve } from "bknd/adapter/cloudflare";

export default serve<Env>({
  // ...
  onBuilt: async (app) => {
    app.server.get("/hello", (c) => c.json({ hello: "world" })); 
  }, 
});

The property app.server is a Hono instance, you can literally anything you can do with Hono.

D1 Sessions (experimental)

D1 now supports to enable global read replication. This allows to reduce latency by reading from the closest region. In order for this to work, D1 has to be started from a bookmark. You can enable this behavior on bknd by setting the d1.session property:

src/index.ts
import { serve } from "bknd/adapter/cloudflare";

export default serve({
  // ...
  d1: {
    // enables D1 sessions
    session: true,
    // (optional) restrict the transport, options: "header" | "cookie"
    // if not specified, it supports both
    transport: "cookie",
    // (optional) choose session constraint if not bookmark present
    // options: "first-primary" | "first-unconstrained"
    first: "first-primary",
  },
});

If bknd is used in a stateful user context (like in a browser), it'll automatically send the session cookie to the server to set the correct bookmark. If you need to manually set the bookmark, you can do so by setting the x-cf-d1-session header:

curl -H "x-cf-d1-session: <bookmark>" ...

Filesystem access with Vite Plugin

The Cloudflare Vite Plugin allows to use Vite with Miniflare to emulate the Cloudflare Workers runtime. This is great, however, unenv disables any Node.js APIs that aren't supported, including the fs module. If you want to use plugins such as syncTypes, this will cause issues.

To fix this, bknd exports a Vite plugin that provides filesystem access during development. You can use it by adding the following to your vite.config.ts file:

import { devFsVitePlugin } from "bknd/adapter/cloudflare";

export default defineConfig({
  plugins: [devFsVitePlugin()], 
});

Now to use this polyfill, you can use the devFsWrite function to write files to the filesystem.

import { devFsWrite } from "bknd/adapter/cloudflare"; 
import { syncTypes } from "bknd/plugins";

export default {
   options: {
      plugins: [
         syncTypes({ 
            write: async (et) => {
               await devFsWrite("bknd-types.d.ts", et.toString()); 
            }
         }),
      ]
   },
} satisfies BkndConfig;

Cloudflare Bindings in CLI

The bknd CLI does not automatically have access to the Cloudflare bindings. We need to manually proxy them to the CLI by using the withPlatformProxy helper function:

bknd.config.ts
import { d1 } from "bknd/adapter/cloudflare";
import { withPlatformProxy } from "bknd/adapter/cloudflare/proxy";

export default withPlatformProxy({
  app: ({ env }) => ({
     connection: d1({ binding: env.DB }),
  }),
});

Now you can use the CLI with your Cloudflare resources.

Make sure to not import from this file in your app, as this would include wrangler as a dependency.

Instead, it's recommended to split this configuration into separate files, e.g. bknd.config.ts and config.ts:

config.ts
import type { CloudflareBkndConfig } from "bknd/adapter/cloudflare";

export default {
  app: ({ env }) => ({
     connection: d1({ binding: env.DB }),
  }),
} satisfies CloudflareBkndConfig;

config.ts now holds the configuration, and can safely be imported in your app. Since the CLI looks for a bknd.config.ts file by default, we change it to wrap the configuration from config.ts in the withPlatformProxy helper function.

bknd.config.ts
import { withPlatformProxy } from "bknd/adapter/cloudflare/proxy";
import config from "./config";

export default withPlatformProxy(config);

As an additional safe guard, you have to set a PROXY environment variable to 1 to enable the proxy.

PROXY=1 npx bknd types