docs: added docs about how to use bknd.config.ts

This commit is contained in:
dswbx
2025-06-05 17:11:50 +02:00
parent 7b128c9701
commit 3e77982996
11 changed files with 566 additions and 94 deletions

153
docs/extending/config.mdx Normal file
View File

@@ -0,0 +1,153 @@
---
title: bknd.config.ts
---
The central configuration file to extend bknd should be placed in the root of your project, so that the [CLI](/usage/cli#using-configuration-file-bknd-config) can automatically pick it up. It allows to:
* define your database connection centrally
* pass in initial configuration or data seeds when booting the first time
* add plugins to the app
* hook into system events
* define custom routes and endpoints
A simple example of a `bknd.config.ts` file:
```typescript bknd.config.ts
import type { BkndConfig } from "bknd/adapter";
export default {
connection: {
url: "file:data.db",
}
} satisfies BkndConfig;
```
## Overview
The `BkndConfig` extends the [`CreateAppConfig`](/usage/introduction#configuration-createappconfig) type with the following properties:
```typescript
export type BkndConfig = CreateAppConfig & {
// return the app configuration as object or from a function
app?: CreateAppConfig | ((args: Args) => CreateAppConfig);
// called before the app is built
beforeBuild?: (app: App) => Promise<void>;
// called after the app has been built
onBuilt?: (app: App) => Promise<void>;
// passed as the first argument to the `App.build` method
buildConfig?: Parameters<App["build"]>[0];
// force the app to be recreated
force?: boolean;
// the id of the app, defaults to `app`
id?: string;
}
```
The supported configuration file extensions are `js`, `ts`, `mjs`, `cjs` and `json`. Throughout the documentation, we'll use `ts` for the file extension.
### `app` (CreateAppConfig)
The `app` property is a function that returns a `CreateAppConfig` object. It allows to pass in the environment variables to the configuration object.
```typescript
import type { BkndConfig } from "bknd/adapter";
export default {
app: ({ env }) => ({
connection: {
url: env.DB_URL,
}
})
} satisfies BkndConfig;
```
See [Database](/usage/database) for more information on how to configure the database connection.
### `beforeBuild`
The `beforeBuild` property is an async function that is called before the app is built. It allows to modify the app instance that may influence the build process.
```typescript
import type { BkndConfig } from "bknd/adapter";
export default {
beforeBuild: async (app: App) => {
// do something that has to happen before the app is built
}
}
```
### `onBuilt`
The `onBuilt` property is an async function that is called after the app has been built. It allows to hook into the app after it has been built. This is useful for defining event listeners or register custom routes, as both the event manager and the server are recreated during the build process.
```typescript
import { type App, AppEvents } from "bknd";
import type { BkndConfig } from "bknd/adapter";
export default {
onBuilt: async (app: App) => {
console.log("App built", app.version());
// registering an event listener
app.emgr.onEvent(AppEvents.AppRequest, (event) => {
console.log("Request received", event.request.url);
})
// registering a custom route
app.server.get("/hello", (c) => c.text("Hello World"));
}
}
```
### `force` & `id`
The `force` property is a boolean that forces the app to be recreated. This is mainly useful for serverless environments where the execution environment is re-used, and you may or may not want to recreate the app on every request.
The `id` property is the reference in a cache map. You may create multiple instances of apps in the same process by using different ids (e.g. multi tenant applications).
## Framework & Runtime configuration
Depending on which framework or runtime you're using to run bknd, the configuration object will extend the `BkndConfig` type with additional properties.
### `RuntimeBkndConfig`
[Runtime adapters](/integration/runtime) need additional configuration to serve static assets for the admin UI.
```typescript
export type RuntimeBkndConfig<Args = any> = BkndConfig<Args> & {
// the path to the dist folder to serve static assets for the admin UI
distPath?: string;
// custom middleware to serve static assets for the admin UI
serveStatic?: MiddlewareHandler | [string, MiddlewareHandler];
// options for the admin UI
adminOptions?: AdminControllerOptions | false;
};
```
### `FrameworkBkndConfig`
[Framework adapters](/integration/framework) may need additional configuration based on the framework's requirements. For example, the `NextjsBkndConfig` type extends the `BkndConfig` type with the following additional properties:
```typescript
type NextjsEnv = NextApiRequest["env"];
export type NextjsBkndConfig<Env = NextjsEnv> = FrameworkBkndConfig<Env> & {
cleanRequest?: { searchParams?: string[] };
};
```
Next.js adds the mounted path to the request object, so that the `cleanRequest` property can be used to remove the mounted path from the request URL. See other frameworks for more information on how to configure them.
## Using the configuration file
The configuration file is automatically picked up if you're using the [CLI](/usage/cli). This allows interacting with your application using the `bknd` command. For example, you can run the following command in the root of your project to start an instance:
```bash
npx bknd run
```
When serving your application, you need to make sure to import the contents of your configuration file. If you're using Next.js for example, it's recommended to follow these steps:
1. create a `bknd.config.ts` file in the root of your project which defines the connection to the database, adds event listeners and custom routes.
2. create a `bknd.ts` file inside your app folder which exports helper functions to instantiate the bknd instance and retrieve the API.
3. create a catch-all route file at `src/api/[[...bknd]]/route.ts` which serves the bknd API.
This way, your application and the CLI are using the same configuration.

127
docs/extending/events.mdx Normal file
View File

@@ -0,0 +1,127 @@
---
title: Events & Hooks
---
bknd comes with a powerful built-in event system that allows you to hook into the app lifecycle and extend its functionality. You can hook into these events in two ways:
- `async`: Your listener is not blocking the main execution flow. E.g. on Cloudflare Workers, by default, the `ExecutionContext`'s `waitUntil` method is used so that the listeners runs after the response is sent.
- `sync`: Your listener is blocking the main execution flow. This allows to abort the request in your custom conditions. Some events also allow to return a modified event payload.
<Note>
Don't mistake `async` with JavaScript's `async` keyword. The `async` keyword is used to indicate that the listener is not blocking the main execution flow.
</Note>
## Overview
### Listening to events
You can listen to events by using the `EventManager` exposed at `app.emgr`. To register a listener, you can use either of these methods:
- `onEvent`: Register a listener for a specific event with a typed event.
- `on`: Register a listener for an event by its slug.
- `onAny`: Register a listener for all events.
```typescript
import { createApp, AppEvents } from "bknd";
const app = createApp();
app.emgr.onEvent(AppEvents.AppRequest, async (event) => {
// ^? AppRequest
console.log("Request received", event.request.url);
});
app.emgr.on("app-request", async (event) => {
console.log("Request received", event.request.url);
});
app.emgr.onAny(async (event) => {
console.log("Event received", event.slug);
});
```
You may want to register your listeners inside [`bknd.config.ts`](/extending/config) to make sure they are registered before the app is built:
```typescript bknd.config.ts
import { AppEvents } from "bknd";
export default {
onBuilt: (app) => {
app.emgr.onEvent(AppEvents.AppRequest, async (event) => {
console.log("Request received", event.request.url);
});
}
}
```
### Setting a mode
By default, listeners are registered as `async` listeners, meaning that the listener is not blocking the main execution flow. You can change this by passing the mode as the third argument:
```typescript
app.emgr.onEvent(AppEvents.AppRequest, async (event) => {
console.log("Request received", event.request.url);
}, { mode: "sync" });
```
This works for all three methods.
## App Events
These events are emitted by the `App` class and are available on the `AppEvents` object.
```typescript
import { AppEvents } from "bknd";
```
Available events:
| Event | Params | Description |
|:------|:--------|:------------|
| `AppConfigUpdatedEvent` | `{ app: App }` | Emitted when the app configuration is updated |
| `AppBuiltEvent` | `{ app: App }` | Emitted when the app is built |
| `AppFirstBoot` | `{ app: App }` | Emitted when the app is first booted |
| `AppRequest` | `{ app: App, request: Request }` | Emitted when a request is received |
| `AppBeforeResponse` | `{ app: App, request: Request, response: Response }` | Emitted before a response is sent |
## Database Events
These events are emitted by the `Database` class and are available on the `DatabaseEvents` object. These are divided by events triggered by the `Mutator` and `Repository` classes.
```typescript
import { DatabaseEvents } from "bknd/data";
```
### Mutator Events
These events are emitted during database mutations (insert, update, delete operations).
| Event | Params | Description |
|:------|:--------|:------------|
| `MutatorInsertBefore` | `{ entity: Entity, data: EntityData }` | Emitted before inserting a new record. Can modify the data. |
| `MutatorInsertAfter` | `{ entity: Entity, data: EntityData, changed: EntityData }` | Emitted after inserting a new record. |
| `MutatorUpdateBefore` | `{ entity: Entity, entityId: PrimaryFieldType, data: EntityData }` | Emitted before updating a record. Can modify the data. |
| `MutatorUpdateAfter` | `{ entity: Entity, entityId: PrimaryFieldType, data: EntityData, changed: EntityData }` | Emitted after updating a record. |
| `MutatorDeleteBefore` | `{ entity: Entity, entityId: PrimaryFieldType }` | Emitted before deleting a record. |
| `MutatorDeleteAfter` | `{ entity: Entity, entityId: PrimaryFieldType, data: EntityData }` | Emitted after deleting a record. |
### Repository Events
These events are emitted during database queries (find operations).
| Event | Params | Description |
|:------|:--------|:------------|
| `RepositoryFindOneBefore` | `{ entity: Entity, options: RepoQuery }` | Emitted before finding a single record. |
| `RepositoryFindOneAfter` | `{ entity: Entity, options: RepoQuery, data: EntityData }` | Emitted after finding a single record. |
| `RepositoryFindManyBefore` | `{ entity: Entity, options: RepoQuery }` | Emitted before finding multiple records. |
| `RepositoryFindManyAfter` | `{ entity: Entity, options: RepoQuery, data: EntityData }` | Emitted after finding multiple records. |
## Media Events
These events are emitted by the `Storage` class and are available on the `MediaEvents` object.
```typescript
import { MediaEvents } from "bknd/media";
```
| Event | Params | Description |
|:------|:--------|:------------|
| `FileUploadedEvent` | `{ file: FileBody } & FileUploadPayload` | Emitted when a file is successfully uploaded. Can modify the event data. |
| `FileDeletedEvent` | `{ name: string }` | Emitted when a file is deleted. |
| `FileAccessEvent` | `{ name: string }` | Emitted when a file is accessed. |
## Auth Events
Coming soon.

View File

@@ -28,14 +28,14 @@ To get started with Astro and bknd you can either install the package manually,
npx astro add react
```
You also need to make sure to set the output to `hybrid` in your Astro config:
You also need to make sure to set the output to `server` in your Astro config:
```js {6}
// astro.config.mjs
import { defineConfig } from "astro/config";
import react from "@astrojs/react";
export default defineConfig({
output: "hybrid",
output: "server",
integrations: [react()]
});
```
@@ -48,8 +48,58 @@ To get started with Astro and bknd you can either install the package manually,
</Tab>
</Tabs>
## Configuration
Now create a `bknd.config.ts` file in the root of your project. If you created the project using the CLI starter, this file is already created for you.
```typescript bknd.config.ts
import type { AstroBkndConfig } from "bknd/adapter/astro";
export default {
connection: {
url: "file:data.db"
},
} satisfies AstroBkndConfig;
```
See [bknd.config.ts](/extending/config) for more information on how to configure bknd. The `AstroBkndConfig` type extends the `BkndConfig` type with the following additional properties:
```typescript
type AstroEnv = NodeJS.ProcessEnv;
export type AstroBkndConfig<Env = AstroEnv> = FrameworkBkndConfig<Env>;
```
## Serve the API
Create a new catch-all route at `src/pages/api/[...api].ts`:
Create a helper file to instantiate the bknd instance and retrieve the API, importing the configurationfrom the `bknd.config.ts` file:
```ts src/bknd.ts
import type { AstroGlobal } from "astro";
import { getApp as getBkndApp } from "bknd/adapter/astro";
import config from "../bknd.config";
export { config };
export async function getApp() {
return await getBkndApp(config);
}
export async function getApi(
astro: AstroGlobal,
opts?: { mode: "static" } | { mode?: "dynamic"; verify?: boolean },
) {
const app = await getApp();
if (opts?.mode !== "static" && opts?.verify) {
const api = app.getApi({ headers: astro.request.headers });
await api.verifyAuth();
return api;
}
return app.getApi();
}
```
Create a new catch-all route at `src/pages/api/[...api].ts`.
```ts src/pages/api/[...api].ts
import { serve } from "bknd/adapter/astro";
@@ -63,6 +113,7 @@ export const ALL = serve({
}
});
```
For more information about the connection object, refer to the [Database](/usage/database) guide. In the
special case of astro, you may also use your Astro DB credentials since it's also using LibSQL
under the hood. Refer to the [Astro DB documentation](https://docs.astro.build/en/guides/astro-db/) for more information.

View File

@@ -22,21 +22,42 @@ To get started with Next.js and bknd you can either install the package manually
</Tab>
</Tabs>
## Configuration
Now create a `bknd.config.ts` file in the root of your project. If you created the project using the CLI starter, this file is already created for you.
```typescript bknd.config.ts
import type { NextjsBkndConfig } from "bknd/adapter/nextjs";
export default {
connection: {
url: "file:data.db"
},
} satisfies NextjsBkndConfig;
```
See [bknd.config.ts](/extending/config) for more information on how to configure bknd. The `NextjsBkndConfig` type extends the `BkndConfig` type with the following additional properties:
```typescript
type NextjsEnv = NextApiRequest["env"];
export type NextjsBkndConfig<Env = NextjsEnv> = FrameworkBkndConfig<Env> & {
cleanRequest?: { searchParams?: string[] };
};
```
## Serve the API
Create a helper file to instantiate the bknd instance and retrieve the API:
Create a helper file to instantiate the bknd instance and retrieve the API, importing the configurationfrom the `bknd.config.ts` file:
```ts src/bknd.ts
import { type NextjsBkndConfig, getApp as getBkndApp } from "bknd/adapter/nextjs";
import { headers } from "next/headers";
import config from "../bknd.config";
export const config = {
connection: {
url: "file:data.db"
},
} as const satisfies NextjsBkndConfig;
export { config };
export async function getApp() {
return await getBkndApp(config);
return await getBkndApp(config, process.env);
}
export async function getApi(opts?: { verify?: boolean }) {

View File

@@ -22,21 +22,44 @@ To get started with React Router and bknd you can either install the package man
</Tab>
</Tabs>
## Configuration
Now create a `bknd.config.ts` file in the root of your project. If you created the project using the CLI starter, this file is already created for you.
```typescript bknd.config.ts
import type { ReactRouterBkndConfig } from "bknd/adapter/react-router";
export default {
connection: {
url: "file:data.db"
},
} satisfies ReactRouterBkndConfig;
```
See [bknd.config.ts](/extending/config) for more information on how to configure bknd. The `ReactRouterBkndConfig` type extends the `BkndConfig` type with the following additional properties:
```typescript
type ReactRouterEnv = NodeJS.ProcessEnv;
type ReactRouterFunctionArgs = {
request: Request;
};
export type ReactRouterBkndConfig<Env = ReactRouterEnv> = FrameworkBkndConfig<Env>;
```
## Serve the API
Create a helper file to instantiate the bknd instance and retrieve the API:
Create a helper file to instantiate the bknd instance and retrieve the API, importing the configurationfrom the `bknd.config.ts` file:
```ts app/bknd.ts
import { type ReactRouterBkndConfig, getApp as getBkndApp } from "bknd/adapter/react-router";
import config from "../bknd.config";
const config = {
connection: {
url: "file:data.db"
}
} as const satisfies ReactRouterBkndConfig;
export { config };
export async function getApp(args?: { request: Request }) {
return await getBkndApp(config, args);
// you may adjust this function depending on your runtime environment.
// e.g. when deploying to cloudflare workers, you'd want the FunctionArgs to be passed in
// to resolve environment variables
export async function getApp() {
return await getBkndApp(config, process.env as any);
}
export async function getApi(
@@ -60,7 +83,7 @@ Create a new api splat route file at `app/routes/api.$.ts`:
import { getApp } from "~/bknd";
const handler = async (args: { request: Request }) => {
const app = await getApp(args);
const app = await getApp();
return app.fetch(args.request);
};

View File

@@ -16,25 +16,42 @@ Additionally, install required dependencies:
npm install @hono/vite-dev-server
```
## Configuration
Now create a `bknd.config.ts` file in the root of your project.
```typescript bknd.config.ts
import type { ViteBkndConfig } from "bknd/adapter/vite";
export default {
connection: {
url: "file:data.db"
}
} satisfies ViteBkndConfig;
```
See [bknd.config.ts](/extending/config) for more information on how to configure bknd.
The `ViteBkndConfig` type extends the `BkndConfig` type with the following additional properties:
```typescript
export type ViteEnv = NodeJS.ProcessEnv;
export type ViteBkndConfig<Env = ViteEnv> = RuntimeBkndConfig<Env> & {};
```
## Serve the API
To serve the **bknd** API, you first have to create a local server file for you vite environment.
Create a `server.ts` file:
```ts
```typescript
import { serve } from "bknd/adapter/vite";
import config from "./bknd.config";
// the configuration given is optional
export default serve({
mode: "cached", // that's the default
connection: {
url: ":memory:"
}
})
export default serve(config)
```
For more information about the connection object, refer to the [Database](/usage/database) guide.
You can also run your vite server in `mode: "fresh"`, this will re-create the app on every fetch.
This is only useful for when working on the `bknd` repository directly.
For more information about the connection object, refer to the [Database](/usage/database) guide.
Next, adjust your `vite.config.ts` to look like the following:
```ts
import { devServer } from "bknd/adapter/vite";
@@ -82,17 +99,22 @@ options you can make use of to adjust it according to your setup.
There might be cases you want to be sure to be in control over the HTML that is being used.
`bknd` generates it automatically, but you use your own one as follows:
```ts server.ts
```typescript server.ts
import { serve, addViteScript } from "bknd/adapter/vite";
import { readFile } from "node:fs/promises"
import config from "./bknd.config";
let html = await readFile("./index.html", "utf-8");
// add vite scripts
html = addViteScript(html);
// then add it as an option
export default serve({ html })
export default serve({
...config,
adminOptions: {
html: addViteScript(html),
// optionally, you can change the base path for the admin UI
adminBasePath: "/admin"
}
})
```
The vite scripts has to be added manually currently, as adding them automatically with
@@ -101,13 +123,18 @@ The vite scripts has to be added manually currently, as adding them automaticall
### Use a custom entry point
By default, the entry point `/src/main.tsx` is used and should fit most cases. If that's not you,
you can supply a different one like so:
```ts server.ts
```typescript server.ts
import { serve } from "bknd/adapter/vite";
import config from "./bknd.config";
// the configuration given is optional
export default serve({
forceDev: {
mainPath: "/src/special.tsx"
}
...config,
adminOptions: {
forceDev: {
mainPath: "/src/special.tsx"
}
}
});
```

View File

@@ -74,6 +74,10 @@
"usage/elements"
]
},
{
"group": "Extending",
"pages": ["extending/config", "extending/events"]
},
{
"group": "Integration",
"pages": [

View File

@@ -14,20 +14,21 @@ Here is the output:
$ npx bknd
Usage: bknd [options] [command]
⚡ bknd cli v0.10.3-rc.1
⚡ bknd cli v0.13.0
Options:
-V, --version output the version number
-h, --help display help for command
Commands:
config [options] get default config
copy-assets [options] copy static assets
create [options] create a new project
debug <subject> debug bknd
run [options] run an instance
user <action> create/update users, or generate a token (auth)
types [options] generate types
schema [options] get schema
user <action> create and update user (auth)
run [options] run an instance
debug <subject> debug bknd
create [options] create a new project
copy-assets [options] copy static assets
config [options] get default config
help [command] display help for command
```
@@ -123,4 +124,71 @@ To start an instance with an ephemeral in-memory database, run the following:
```
npx bknd run --memory
```
Keep in mind that the database is not persisted and will be lost when the process is terminated.
Keep in mind that the database is not persisted and will be lost when the process is terminated.
## Generating types (`types`)
To see all available `types` options, execute `npx bknd types --help`.
```
$ npx bknd types --help
Usage: bknd types [options]
generate types
Options:
-o, --outfile <outfile> output file (default: "bknd-types.d.ts")
--no-write do not write to file
-h, --help display help for command
```
To generate types for the database, run the following:
```
npx bknd types
```
This will generate types for your database schema in `bknd-types.d.ts`. The generated file could look like this:
```typescript bknd-types.d.ts
import type { DB } from "bknd/core";
import type { Insertable, Selectable, Updateable, Generated } from "kysely";
declare global {
type BkndEntity<T extends keyof DB> = Selectable<DB[T]>;
type BkndEntityCreate<T extends keyof DB> = Insertable<DB[T]>;
type BkndEntityUpdate<T extends keyof DB> = Updateable<DB[T]>;
}
export interface Todos {
id: Generated<number>;
title?: string;
done?: boolean;
}
interface Database {
todos: Todos;
}
declare module "bknd/core" {
interface DB extends Database {}
}
```
Make sure to add the generated file in your `tsconfig.json` file:
```json tsconfig.json
{
"include": [
"bknd-types.d.ts"
]
}
```
You can then use the types by importing them from `bknd/core`:
```typescript
import type { DB } from "bknd/core";
type Todo = DB["todos"];
```
All bknd methods that involve your database schema will be automatically typed.

View File

@@ -18,7 +18,7 @@ There is also a fourth option, which is running it inside a
Regardless of the method you choose, at the end all adapters come down to the actual
instantiation of the `App`, which in raw looks like this:
```ts
```typescript
import { createApp, type CreateAppConfig } from "bknd";
// create the app
@@ -38,7 +38,7 @@ implements the `Fetch` API.
## Configuration (`CreateAppConfig`)
The `CreateAppConfig` type is the main configuration object for the `createApp` function. It has
the following properties:
```ts
```typescript
import type { App, InitialModuleConfigs, ModuleBuildContext } from "bknd";
import type { Connection } from "bknd/data";
import type { Config } from "@libsql/client";