diff --git a/app/src/ui/Admin.tsx b/app/src/ui/Admin.tsx
index 0da2b90..17263bb 100644
--- a/app/src/ui/Admin.tsx
+++ b/app/src/ui/Admin.tsx
@@ -8,7 +8,33 @@ import * as AppShell from "ui/layouts/AppShell/AppShell";
import { ClientProvider, useBkndWindowContext, type ClientProviderProps } from "./client";
import { createMantineTheme } from "./lib/mantine/theme";
import { Routes } from "./routes";
-import type { BkndAdminAppShellOptions, BkndAdminEntitiesOptions } from "ui/options";
+import type { BkndAdminAppShellOptions, BkndAdminEntitiesOptions } from "./options";
+
+export type BkndAdminConfig = {
+ /**
+ * Base path of the Admin UI
+ * @default `/`
+ */
+ basepath?: string;
+ /**
+ * Path to return to when clicking the logo
+ * @default `/`
+ */
+ logo_return_path?: string;
+ /**
+ * Theme of the Admin UI
+ * @default `system`
+ */
+ theme?: AppTheme;
+ /**
+ * Entities configuration like headers, footers, actions, field renders, etc.
+ */
+ entities?: BkndAdminEntitiesOptions;
+ /**
+ * App shell configuration like user menu actions.
+ */
+ appShell?: BkndAdminAppShellOptions;
+};
export type BkndAdminProps = {
/**
@@ -16,37 +42,13 @@ export type BkndAdminProps = {
*/
baseUrl?: string;
/**
- * Whether to wrap Admin in a
+ * Whether to wrap Admin in a ``
*/
withProvider?: boolean | ClientProviderProps;
/**
* Admin UI customization options
*/
- config?: {
- /**
- * Base path of the Admin UI
- * @default `/`
- */
- basepath?: string;
- /**
- * Path to return to when clicking the logo
- * @default `/`
- */
- logo_return_path?: string;
- /**
- * Theme of the Admin UI
- * @default `system`
- */
- theme?: AppTheme;
- /**
- * Entities configuration like headers, footers, actions, field renders, etc.
- */
- entities?: BkndAdminEntitiesOptions;
- /**
- * App shell configuration like user menu actions.
- */
- appShell?: BkndAdminAppShellOptions;
- };
+ config?: BkndAdminConfig;
children?: ReactNode;
};
diff --git a/app/src/ui/index.ts b/app/src/ui/index.ts
index 5a3f15b..50c734f 100644
--- a/app/src/ui/index.ts
+++ b/app/src/ui/index.ts
@@ -1,4 +1,4 @@
-export { default as Admin, type BkndAdminProps } from "./Admin";
+export { default as Admin, type BkndAdminProps, type BkndAdminConfig } from "./Admin";
export * from "./components/form/json-schema-form";
export { JsonViewer } from "./components/code/JsonViewer";
export type * from "./options";
diff --git a/docs/content/docs/(documentation)/extending/admin.mdx b/docs/content/docs/(documentation)/extending/admin.mdx
new file mode 100644
index 0000000..a2adede
--- /dev/null
+++ b/docs/content/docs/(documentation)/extending/admin.mdx
@@ -0,0 +1,201 @@
+---
+title: Admin UI
+tags: ["documentation"]
+---
+
+import { TypeTable } from "fumadocs-ui/components/type-table";
+
+
+bknd features an integrated Admin UI that can be used to:
+- fully manage your backend visually when run in [`db` mode](/usage/introduction/#ui-only-mode)
+- manage your database contents
+- manage your media contents
+
+In case you're using bknd with a [React framework](integration/introduction/#start-with-a-framework) and render the Admin as React component, you can go further and customize the Admin UI to your liking.
+
+
+
+
+## Advanced Example
+
+The following example shows how to customize the Admin UI for each entity.
+
+- adds a custom action item to the user menu (top right)
+- adds a custom action item to the entity list
+- adds a custom action item to the entity create/update form
+- overrides the rendering of the title field
+- renders a custom header for the entity
+- renders a custom footer for the entity
+- adds a custom route
+
+```tsx
+import { Admin } from "bknd/ui";
+import { Route } from "wouter";
+
+export function App() {
+ return (
+ alert("custom"),
+ },
+ ],
+ },
+ entities: {
+ // use any entity that is registered
+ tests: {
+ actions: (context, entity, data) => ({
+ primary: [
+ // this action is only rendered in the update context
+ context === "update" && {
+ children: "another",
+ onClick: () => alert("another"),
+ },
+ ],
+ context: [
+ // this action is always rendered in the dropdown
+ {
+ label: "Custom",
+ onClick: () =>
+ alert(
+ "custom: " +
+ JSON.stringify({ context, entity: entity.name, data }),
+ ),
+ },
+ ],
+ }),
+ // render a header for the entity
+ header: (context, entity, data) =>
test header
,
+ // override the rendering of the title field
+ fields: {
+ title: {
+ render: (context, entity, field, ctx) => {
+ return (
+ ctx.handleChange(e.target.value)}
+ />
+ );
+ },
+ },
+ },
+ },
+ // system entities work too
+ users: {
+ header: () => {
+ return
System entity
;
+ },
+ },
+ },
+ }}
+ >
+ {/* You may also add custom routes, these always have precedence over the Admin routes */}
+
+
custom
+
+
+ );
+}
+```
+
+
+## `config`
+
+
+
+### `entities`
+
+With the `entities` option, you can customize the Admin UI for each entity. You can override the header, footer, add additional actions, and override each field rendering.
+
+```ts
+export type BkndAdminEntityContext = "list" | "create" | "update";
+
+export type BkndAdminEntitiesOptions = {
+ [E in keyof DB]?: BkndAdminEntityOptions;
+};
+
+export type BkndAdminEntityOptions = {
+ /**
+ * Header to be rendered depending on the context
+ */
+ header?: (
+ context: BkndAdminEntityContext,
+ entity: Entity,
+ data?: DB[E],
+ ) => ReactNode | void | undefined;
+ /**
+ * Footer to be rendered depending on the context
+ */
+ footer?: (
+ context: BkndAdminEntityContext,
+ entity: Entity,
+ data?: DB[E],
+ ) => ReactNode | void | undefined;
+ /**
+ * Actions to be rendered depending on the context
+ */
+ actions?: (
+ context: BkndAdminEntityContext,
+ entity: Entity,
+ data?: DB[E],
+ ) => {
+ /**
+ * Primary actions are always visible
+ */
+ primary?: (ButtonProps | undefined | null | false)[];
+ /**
+ * Context actions are rendered in a dropdown
+ */
+ context?: DropdownProps["items"];
+ };
+ /**
+ * Field UI overrides
+ */
+ fields?: {
+ [F in keyof DB[E]]?: BkndAdminEntityFieldOptions;
+ };
+};
+
+export type BkndAdminEntityFieldOptions = {
+ /**
+ * Override the rendering of a certain field
+ */
+ render?: (
+ context: BkndAdminEntityContext,
+ entity: Entity,
+ field: Field,
+ ctx: {
+ data?: DB[E];
+ value?: DB[E][keyof DB[E]];
+ handleChange: (value: any) => void;
+ },
+ ) => ReactNode | void | undefined;
+};
+```
+
+
+### `appShell`
+
+```ts
+export type DropdownItem =
+ | (() => ReactNode)
+ | {
+ label: string | ReactElement;
+ icon?: any;
+ onClick?: () => void;
+ destructive?: boolean;
+ disabled?: boolean;
+ title?: string;
+ [key: string]: any;
+ };
+
+export type BkndAdminAppShellOptions = {
+ userMenu?: (DropdownItem | undefined | boolean)[];
+};
+```
\ No newline at end of file
diff --git a/docs/content/docs/(documentation)/extending/config.mdx b/docs/content/docs/(documentation)/extending/config.mdx
index 8f81579..30c63f8 100644
--- a/docs/content/docs/(documentation)/extending/config.mdx
+++ b/docs/content/docs/(documentation)/extending/config.mdx
@@ -23,26 +23,6 @@ export default {
} 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;
- // called after the app has been built
- onBuilt?: (app: App) => Promise;
- // passed as the first argument to the `App.build` method
- buildConfig?: Parameters[0];
-};
-```
-
The supported configuration file extensions are `js`, `ts`, `mjs`, `cjs` and `json`. Throughout the documentation, we'll use `ts` for the file extension.
## Example
@@ -74,7 +54,177 @@ export default {
} satisfies BkndConfig;
```
-### `app` (CreateAppConfig)
+
+## Configuration (`BkndConfig`)
+
+The `BkndConfig` type is the main configuration object for the `createApp` function. It has
+the following properties:
+
+```typescript
+import type { App, InitialModuleConfigs, ModuleBuildContext, Connection, MaybePromise } from "bknd";
+import type { Config } from "@libsql/client";
+
+type AppPlugin = (app: App) => Promise | void;
+type ManagerOptions = {
+ basePath?: string;
+ trustFetched?: boolean;
+ onFirstBoot?: () => Promise;
+ seed?: (ctx: ModuleBuildContext) => Promise;
+};
+
+type BkndConfig = {
+ connection?: Connection | Config;
+ config?: InitialModuleConfigs;
+ options?: {
+ plugins?: AppPlugin[];
+ manager?: ManagerOptions;
+ };
+ app?: BkndConfig | ((args: Args) => MaybePromise>);
+ onBuilt?: (app: App) => Promise;
+ beforeBuild?: (app?: App) => Promise;
+ buildConfig?: {
+ sync?: boolean;
+ }
+};
+```
+
+### `connection`
+
+The `connection` property is the main connection object to the database. It can be either an object with libsql config or the actual `Connection` class.
+
+```ts
+// uses the default SQLite connection depending on the runtime
+const connection = { url: "" };
+
+// the same as above, but more explicit
+import { sqlite } from "bknd/adapter/sqlite";
+const connection = sqlite({ url: "" });
+
+// Node.js SQLite, default on Node.js
+import { nodeSqlite } from "bknd/adapter/node";
+const connection = nodeSqlite({ url: "" });
+
+// Bun SQLite, default on Bun
+import { bunSqlite } from "bknd/adapter/bun";
+const connection = bunSqlite({ url: "" });
+
+// LibSQL, default on Cloudflare
+import { libsql } from "bknd";
+const connection = libsql({ url: "" });
+```
+
+See a full list of available connections in the [Database](/usage/database) section. Alternatively, you can pass an instance of a `Connection` class directly, see [Custom Connection](/usage/database#custom-connection) as a reference.
+
+If the connection object is omitted, the app will try to use an in-memory database.
+
+### `config`
+
+As configuration, you can either pass a partial configuration object or a complete one
+with a version number. The version number is used to automatically migrate the configuration up
+to the latest version upon boot ([`db` mode](/usage/introduction#ui-only-mode) only). The default configuration looks like this:
+
+```json
+{
+ "server": {
+ "cors": {
+ "origin": "*",
+ "allow_methods": [
+ "GET",
+ "POST",
+ "PATCH",
+ "PUT",
+ "DELETE"
+ ],
+ "allow_headers": [
+ "Content-Type",
+ "Content-Length",
+ "Authorization",
+ "Accept"
+ ],
+ "allow_credentials": true
+ },
+ "mcp": {
+ "enabled": false,
+ "path": "/api/system/mcp",
+ "logLevel": "emergency"
+ }
+ },
+ "data": {
+ "basepath": "/api/data",
+ "default_primary_format": "integer",
+ "entities": {},
+ "relations": {},
+ "indices": {}
+ },
+ "auth": {
+ "enabled": false,
+ "basepath": "/api/auth",
+ "entity_name": "users",
+ "allow_register": true,
+ "jwt": {
+ "secret": "",
+ "alg": "HS256",
+ "expires": 0,
+ "issuer": "",
+ "fields": [
+ "id",
+ "email",
+ "role"
+ ]
+ },
+ "cookie": {
+ "path": "/",
+ "sameSite": "strict",
+ "secure": true,
+ "httpOnly": true,
+ "expires": 604800,
+ "partitioned": false,
+ "renew": true,
+ "pathSuccess": "/",
+ "pathLoggedOut": "/"
+ },
+ "strategies": {
+ "password": {
+ "type": "password",
+ "enabled": true,
+ "config": {
+ "hashing": "sha256"
+ }
+ }
+ },
+ "guard": {
+ "enabled": false
+ },
+ "roles": {}
+ },
+ "media": {
+ "enabled": false,
+ "basepath": "/api/media",
+ "entity_name": "media",
+ "storage": {}
+ },
+ "flows": {
+ "basepath": "/api/flows",
+ "flows": {}
+ }
+}
+```
+
+You can use the [CLI](/usage/cli/#getting-the-configuration-config) to get the default configuration:
+
+```sh
+npx bknd config --default --pretty
+```
+
+To validate your configuration against a JSON schema, you can also dump the schema using the CLI:
+
+```sh
+npx bknd schema
+```
+
+To create an initial data structure, you can use helpers [described here](/usage/database#data-structure).
+
+### `app`
The `app` property is a function that returns a `CreateAppConfig` object. It allows accessing the adapter specific environment variables. This is especially useful when using the [Cloudflare Workers](/integration/cloudflare) runtime, where the environment variables are only available inside the request handler.
@@ -129,6 +279,52 @@ export default {
};
```
+### `options.plugins`
+
+The `plugins` property is an array of functions that are called after the app has been built,
+but before its event is emitted. This is useful for adding custom routes or other functionality.
+A simple plugin that adds a custom route looks like this:
+
+```ts
+import type { AppPlugin } from "bknd";
+
+export const myPlugin: AppPlugin = (app) => {
+ app.server.get("/hello", (c) => c.json({ hello: "world" }));
+};
+```
+
+Since each plugin has full access to the `app` instance, it can add routes, modify the database
+structure, add custom middlewares, respond to or add events, etc. Plugins are very powerful, so
+make sure to only run trusted ones.
+
+Read more about plugins in the [Plugins](/extending/plugins) section.
+
+### `options.seed`
+
+
+ The seed function will only be executed on app's first boot in `"db"` mode. If a configuration already exists in the database, or in `"code"` mode, it will not be executed.
+
+
+The `seed` property is a function that is called when the app is booted for the first time. It is used to seed the database with initial data. The function is passed a `ModuleBuildContext` object:
+
+```ts
+type ModuleBuildContext = {
+ connection: Connection;
+ server: Hono;
+ em: EntityManager;
+ emgr: EventManager;
+ guard: Guard;
+};
+
+const seed = async (ctx: ModuleBuildContext) => {
+ // seed the database
+ await ctx.em.mutator("todos").insertMany([
+ { title: "Learn bknd", done: true },
+ { title: "Build something cool", done: false },
+ ]);
+};
+```
+
## 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.
diff --git a/docs/content/docs/(documentation)/meta.json b/docs/content/docs/(documentation)/meta.json
index b2d5db7..442c37d 100644
--- a/docs/content/docs/(documentation)/meta.json
+++ b/docs/content/docs/(documentation)/meta.json
@@ -20,6 +20,7 @@
"./extending/config",
"./extending/events",
"./extending/plugins",
+ "./extending/admin",
"---Integration---",
"./integration/introduction",
"./integration/(frameworks)/",
diff --git a/docs/content/docs/(documentation)/usage/introduction.mdx b/docs/content/docs/(documentation)/usage/introduction.mdx
index 472d0d1..ebac844 100644
--- a/docs/content/docs/(documentation)/usage/introduction.mdx
+++ b/docs/content/docs/(documentation)/usage/introduction.mdx
@@ -184,197 +184,4 @@ To keep your config, secrets and types in sync, you can either use the CLI or th
-## Configuration (`BkndConfig`)
-
-The `BkndConfig` type is the main configuration object for the `createApp` function. It has
-the following properties:
-
-```typescript
-import type { App, InitialModuleConfigs, ModuleBuildContext, Connection, MaybePromise } from "bknd";
-import type { Config } from "@libsql/client";
-
-type AppPlugin = (app: App) => Promise | void;
-type ManagerOptions = {
- basePath?: string;
- trustFetched?: boolean;
- onFirstBoot?: () => Promise;
- seed?: (ctx: ModuleBuildContext) => Promise;
-};
-
-type BkndConfig = {
- connection?: Connection | Config;
- config?: InitialModuleConfigs;
- options?: {
- plugins?: AppPlugin[];
- manager?: ManagerOptions;
- };
- app?: BkndConfig | ((args: Args) => MaybePromise>);
- onBuilt?: (app: App) => Promise;
- beforeBuild?: (app?: App) => Promise;
- buildConfig?: {
- sync?: boolean;
- }
-};
-```
-
-### `connection`
-
-The `connection` property is the main connection object to the database. It can be either an object with libsql config or the actual `Connection` class.
-
-```ts
-// uses the default SQLite connection depending on the runtime
-const connection = { url: "" };
-
-// the same as above, but more explicit
-import { sqlite } from "bknd/adapter/sqlite";
-const connection = sqlite({ url: "" });
-
-// Node.js SQLite, default on Node.js
-import { nodeSqlite } from "bknd/adapter/node";
-const connection = nodeSqlite({ url: "" });
-
-// Bun SQLite, default on Bun
-import { bunSqlite } from "bknd/adapter/bun";
-const connection = bunSqlite({ url: "" });
-
-// LibSQL, default on Cloudflare
-import { libsql } from "bknd";
-const connection = libsql({ url: "" });
-```
-
-See a full list of available connections in the [Database](/usage/database) section. Alternatively, you can pass an instance of a `Connection` class directly, see [Custom Connection](/usage/database#custom-connection) as a reference.
-
-If the connection object is omitted, the app will try to use an in-memory database.
-
-### `config`
-
-As [initial configuration](/usage/database#initial-structure), you can either pass a partial configuration object or a complete one
-with a version number. The version number is used to automatically migrate the configuration up
-to the latest version upon boot. The default configuration looks like this:
-
-```json
-{
- "server": {
- "admin": {
- "basepath": "",
- "color_scheme": "light",
- "logo_return_path": "/"
- },
- "cors": {
- "origin": "*",
- "allow_methods": ["GET", "POST", "PATCH", "PUT", "DELETE"],
- "allow_headers": [
- "Content-Type",
- "Content-Length",
- "Authorization",
- "Accept"
- ]
- }
- },
- "data": {
- "basepath": "/api/data",
- "entities": {},
- "relations": {},
- "indices": {}
- },
- "auth": {
- "enabled": false,
- "basepath": "/api/auth",
- "entity_name": "users",
- "allow_register": true,
- "jwt": {
- "secret": "",
- "alg": "HS256",
- "fields": ["id", "email", "role"]
- },
- "cookie": {
- "path": "/",
- "sameSite": "lax",
- "secure": true,
- "httpOnly": true,
- "expires": 604800,
- "renew": true,
- "pathSuccess": "/",
- "pathLoggedOut": "/"
- },
- "strategies": {
- "password": {
- "type": "password",
- "config": {
- "hashing": "sha256"
- }
- }
- },
- "roles": {}
- },
- "media": {
- "enabled": false,
- "basepath": "/api/media",
- "entity_name": "media",
- "storage": {}
- },
- "flows": {
- "basepath": "/api/flows",
- "flows": {}
- }
-}
-```
-
-You can use the [CLI](/usage/cli/#getting-the-configuration-config) to get the default configuration:
-
-```sh
-npx bknd config --default --pretty
-```
-
-To validate your configuration against a JSON schema, you can also dump the schema using the CLI:
-
-```sh
-npx bknd schema
-```
-
-To create an initial data structure, you can use helpers [described here](/usage/database#initial-structure).
-
-### `options.plugins`
-
-The `plugins` property is an array of functions that are called after the app has been built,
-but before its event is emitted. This is useful for adding custom routes or other functionality.
-A simple plugin that adds a custom route looks like this:
-
-```ts
-import type { AppPlugin } from "bknd";
-
-export const myPlugin: AppPlugin = (app) => {
- app.server.get("/hello", (c) => c.json({ hello: "world" }));
-};
-```
-
-Since each plugin has full access to the `app` instance, it can add routes, modify the database
-structure, add custom middlewares, respond to or add events, etc. Plugins are very powerful, so
-make sure to only run trusted ones.
-
-### `options.seed`
-
-
- The seed function will only be executed on app's first boot in `"db"` mode. If a configuration already exists in the database, or in `"code"` mode, it will not be executed.
-
-
-The `seed` property is a function that is called when the app is booted for the first time. It is used to seed the database with initial data. The function is passed a `ModuleBuildContext` object:
-
-```ts
-type ModuleBuildContext = {
- connection: Connection;
- server: Hono;
- em: EntityManager;
- emgr: EventManager;
- guard: Guard;
-};
-
-const seed = async (ctx: ModuleBuildContext) => {
- // seed the database
- await ctx.em.mutator("todos").insertMany([
- { title: "Learn bknd", done: true },
- { title: "Build something cool", done: false },
- ]);
-};
-```