bknd logo

Plugins

bknd allows you to extend its functionality by creating plugins. These allows to hook into the app lifecycle and to provide a data structure that is guaranteed to be merged. A plugin is a function that takes in an instance of App and returns the following structure:

PropTypeDefault
name
string
-
schema?
(() => MaybePromise<any>)
-
beforeBuild?
(() => MaybePromise<void>)
-
onBuilt?
(() => MaybePromise<void>)
-
onServerInit?
((server: Hono<ServerEnv>) => MaybePromise<void>)
-
onBoot?
(() => MaybePromise<void>)
-
onFirstBoot?
(() => MaybePromise<void>)
-

Creating a simple plugin

To create a simple plugin which guarantees an entity pages to be available and an additioanl endpoint to render a html list of pages, you can create it as follows:

myPagesPlugin.tsx
/** @jsxImportSource hono/jsx */
import { type App, type AppPlugin, em, entity, text } from "bknd";

export const myPagesPlugin: AppPlugin = (app) => ({
   name: "my-pages-plugin",
   // define the schema of the plugin
   // this will always be merged into the app's schema
   schema: () => em({
      pages: entity("pages", {
         title: text(),
         content: text(),
      }),
   }),
   // execute code after the app is built
   onBuilt: () => {
      // register a new endpoint, make sure that you choose an endpoint that is reachable for bknd
      app.server.get("/my-pages", async (c) => {
         const { data: pages } = await app.em.repo("pages").findMany({});
         return c.html(
            <body>
               <h1>Pages: {pages.length}</h1>
               <ul>
                  {pages.map((page: any) => (
                     <li key={page.id}>{page.title}</li>
                  ))}
               </ul>
            </body>,
         );
      });
   },
});

And then register it in your bknd.config.ts file:

import type { BkndConfig } from "bknd/adapter";
import { myPagesPlugin } from "./myPagesPlugin";

export default {
   options: {
      plugins: [myPagesPlugin],
   }
} satisfies BkndConfig;

The schema returned from the plugin will be merged into the schema of the app.

Built-in plugins

bknd comes with a few built-in plugins that you can use.

syncTypes

A simple plugin that writes down the TypeScript types of the data schema on boot and each build. The output is equivalent to running npx bknd types.

bknd.config.ts
import { syncTypes } from "bknd/plugins";
import { writeFile } from "node:fs/promises";

export default {
   options: {
      plugins: [
         syncTypes({ 
            // whether to enable the plugin, make sure to disable in production
            enabled: true,
            // your writing function (required)
            write: async (et) => {
               await writeFile("bknd-types.d.ts", et.toString(), "utf-8");
            }
         }),
      ]
   },
} satisfies BkndConfig;

syncConfig

A simple plugin that writes down the app configuration on boot and each build.

bknd.config.ts
import { syncConfig } from "bknd/plugins";
import { writeFile } from "node:fs/promises";

export default {
   options: {
      plugins: [
         syncConfig({ 
            // whether to enable the plugin, make sure to disable in production
            enabled: true,
            // your writing function (required)
            write: async (config) => {
               await writeFile("config.json", JSON.stringify(config, null, 2), "utf-8");
            },
         }),
      ]
   },
} satisfies BkndConfig;

syncSecrets

A simple plugin that writes down the app secrets on boot and each build.

bknd.config.ts
import { syncSecrets } from "bknd/plugins";
import { writeFile } from "node:fs/promises";

export default {
   options: {
      plugins: [
         syncSecrets({ 
            // whether to enable the plugin, make sure to disable in production
            enabled: true,
            // your writing function (required)
            write: async (secrets) => {
               // apply your writing logic, e.g. writing a template file in an env format
               await writeFile(
                  ".env.example",
                  Object.entries(secrets)
                     .map(([key]) => `${key}=`)
                     .join("\n")
               );
            },
         }),
      ]
   },
} satisfies BkndConfig;

showRoutes

A simple plugin that logs the routes of your app in the console.

bknd.config.ts
import { showRoutes } from "bknd/plugins";

export default {
   options: {
      plugins: [
         showRoutes({ 
            // whether to show the routes only once (on first build)
            once: true 
         })
      ],
   },
} satisfies BkndConfig;

cloudflareImageOptimization

This plugin doesn't work on the development server, or on workers deployed with a workers.dev subdomain. It requires Cloudflare Image transformations to be enabled on your zone.

A plugin that add Cloudflare Image Optimization to your app's media storage.

bknd.config.ts
import { cloudflareImageOptimization } from "bknd/plugins";

export default {
   options: {
      plugins: [
         cloudflareImageOptimization({
            // the url to access the image optimization plugin
            accessUrl: "/api/plugin/image/optimize",
            // the path to resolve the image from, defaults to `/api/media/file`
            resolvePath: "/api/media/file",
            // for example, you may want to have default option to limit to a width of 1000px
            defaultOptions: {
               width: 1000,
            }
         })
      ],
   },
} satisfies BkndConfig;

Here is a break down of all configuration options:

PropTypeDefault
accessUrl?
string
/api/plugin/image/optimize
resolvePath?
string
/api/media/file
explain?
boolean
false
defaultOptions?
any
{}
fixedOptions?
any
{}
cacheControl?
string
public, max-age=31536000, immutable
fallbackRedirect?
boolean
false

When enabled, you can now access your images at your configured accessUrl. For example, if you have a media file at /api/media/file/image.jpg, you can access the optimized image at /api/plugin/image/optimize/image.jpg for optimization.

Now you can add query parameters for the transformations, e.g. ?width=1000&height=1000.

PropTypeDefault
dpr?
number
1
fit?
"scale-down" | "contain" | "cover" | "crop" | "pad"
-
format?
"auto" | "avif" | "webp" | "jpeg" | "baseline-jpeg" | "json"
-
height?
number
-
width?
number
-
metadata?
"copyright" | "keep" | "none"
copyright
quality?
number
85

timestamps

A plugin that adds created_at and updated_at fields to the specified entities.

bknd.config.ts
import { timestamps } from "bknd/plugins";

export default {
   options: {
      plugins: [
         timestamps({ 
            // the entities to add timestamps to
            entities: ["pages"],
            // whether to set the `updated_at` field on create, defaults to true
            setUpdatedOnCreate: true,
         })
      ],
   },
} satisfies BkndConfig;

emailOTP

Make sure to setup proper permissions to restrict reading from the OTP entity. Also, this plugin requires the email driver to be registered.

A plugin that adds email OTP functionality to your app. It will add two endpoints to your app:

  • POST /api/auth/otp/login to login a user with an OTP code
  • POST /api/auth/otp/register to register a user with an OTP code

Both endpoints accept a JSON body with email (required) and code (optional). If code is provided, the OTP code will be validated and the user will be logged in or registered. If code is not provided, a new OTP code will be generated and sent to the user's email.

For example, to login an existing user with an OTP code, two requests are needed. The first one only with the email to generate and send the OTP code, and the second to send the users' email along with the OTP code. The last request will authenticate the user.

Generate OTP code to login
POST /api/auth/otp/login
Content-Type: application/json

{
   "email": "test@example.com"
}

If the user exists, an email will be sent with the OTP code, and the response will be a 201 Created.

Login with OTP code
POST /api/auth/otp/login
Content-Type: application/json

{
   "email": "test@example.com",
   "code": "123456"
}

If the code is valid, the user will be authenticated by sending a Set-Cookie header and a body property token with the JWT token (equally to the login endpoint).

bknd.config.ts
import { emailOTP } from "bknd/plugins";
import { resendEmail } from "bknd";

export default {
   options: {
      drivers: {
         // an email driver is required
         email: resendEmail({ /* ... */}),
      },
      plugins: [
         // all options are optional
         emailOTP({
            // the base path for the API endpoints
            apiBasePath: "/api/auth/otp",
            // the TTL for the OTP tokens in seconds
            ttl: 600,
            // the name of the OTP entity
            entity: "users_otp",
            // customize the email content
            generateEmail: (otp) => ({
               subject: "OTP Code",
               body: `Your OTP code is: ${otp.code}`,
            }),
            // customize the code generation
            generateCode: (user) => {
               return Math.floor(100000 + Math.random() * 900000).toString();
            },
         })
      ],
   },
} satisfies BkndConfig;
PropTypeDefault
generateCode?
((user: Pick<DB, "email">) => string)
-
apiBasePath?
string
"/api/auth/otp"
ttl?
number
600 (10 minutes)
entity?
string
"users_otp"
entityConfig?
any
-
generateEmail?
((otp: FieldSchema<{ action: any; code: any; email: any; created_at: any; expires_at: any; used_at: any; }>) => MaybePromise<{ subject: string; body: string | { text: string; html: string; }; }>)
-
showActualErrors?
boolean
false
allowExternalMutations?
boolean
false
sendEmail?
boolean
true