---
title: Plugins
tags: ["documentation"]
---
import { TypeTable } from 'fumadocs-ui/components/type-table';


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:

<AutoTypeTable path="../app/src/App.ts" name="AppPluginConfig" />

## 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:

```tsx title="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:

```typescript
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`.

```typescript title="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.

```typescript title="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.

```typescript title="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.

```typescript title="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`

<Callout type="info">
  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](https://developers.cloudflare.com/images/get-started/#enable-transformations-on-your-zone) on your zone.
</Callout>

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

```typescript title="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:

<AutoTypeTable path="../app/src/plugins/cloudflare/image-optimization.plugin.ts" name="CloudflareImageOptimizationOptions" />

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

<TypeTable
  type={{
    dpr: {
      description:
        'The device pixel ratio to use',
      type: 'number',
      default: 1,
    },
    fit: {
      description: 'The fit mode to use',
      type: '"scale-down" | "contain" | "cover" | "crop" | "pad"',
    },
    format: {
      description: 'The format to use',
      type: '"auto" | "avif" | "webp" | "jpeg" | "baseline-jpeg" | "json"'
    },
    height: {
      description: 'The height to use',
      type: 'number',
    },
    width: {
      description: 'The width to use',
      type: 'number',
    },
    metadata: {
      description: 'The metadata to use',
      type: '"copyright" | "keep" | "none"',
      default: 'copyright'
    },
    quality: {
      description: 'The quality to use',
      type: 'number',
      default: 85
    },
  }}
/>

### `timestamps`

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

```typescript title="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`

<Callout type="warning">
  Make sure to setup proper permissions to restrict reading from the OTP entity. Also, this plugin requires the `email` driver to be registered.
</Callout>


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.

```http title="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`.

```http title="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).


```typescript title="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;
```

<AutoTypeTable path="../app/src/plugins/auth/email-otp.plugin.ts" name="EmailOTPPluginOptions" />
