Release 0.16 (#196)

* initial refactor

* fixes

* test secrets extraction

* updated lock

* fix secret schema

* updated schemas, fixed tests, skipping flow tests for now

* added validator for rjsf, hook form via standard schema

* removed @sinclair/typebox

* remove unneeded vite dep

* fix jsonv literal on Field.tsx

* fix schema import path

* fix schema modals

* fix schema modals

* fix json field form, replaced auth form

* initial waku

* finalize waku example

* fix jsonv-ts version

* fix schema updates with falsy values

* fix media api to respect options' init, improve types

* checking media controller test

* checking media controller test

* checking media controller test

* clean up mediacontroller test

* added cookie option `partitioned`, as well as cors `origin` to be array, option to enable `credentials` (#214)

* added cookie option `partitioned`, as well as cors `origin` to be array, option to enable `credentials`

* fix server test

* fix data api (updated jsonv-ts)

* enhance cloudflare image optimization plugin with new options and explain endpoint (#215)

* feat: add ability to serve static by using dynamic imports (#197)

* feat: add ability to serve static by using dynamic imports

* serveStaticViaImport: make manifest optional

* serveStaticViaImport: add error log

* refactor/imports (#217)

* refactored core and core/utils imports

* refactored core and core/utils imports

* refactored media imports

* refactored auth imports

* refactored data imports

* updated package json exports, fixed mm config

* fix tests

* feat/deno (#219)

* update bun version

* fix module manager's em reference

* add basic deno example

* finalize

* docs: fumadocs migration (#185)

* feat(docs): initialize documentation structure with Fumadocs

* feat(docs): remove home route and move /docs route to /route

* feat(docs): add redirect to /start page

* feat(docs): migrate Getting Started chapters

* feat(docs): migrate Usage and Extending chapters

* feat(callout): add CalloutCaution, CalloutDanger, CalloutInfo, and CalloutPositive

* feat(layout): add Discord and GitHub links to documentation layout

* feat(docs): add integration chapters draft

* feat(docs): add modules chapters draft

* refactor(mdx-components): remove unused Icon import

* refactor(StackBlitz): enhance type safety by using unknown instead of any

* refactor(layout): update navigation mode to 'top' in layout configuration

* feat(docs): add @iconify/react package

* docs(mdx-components): add Icon component to MDX components list

* feat(docs): update Next.js integration guide

* feat(docs): update React Router integration guide

* feat(docs): update Astro integration guide

* feat(docs): update Vite integration guide

* fix(docs): update package manager initialization commands

* feat(docs): migrate Modules chapters

* chore(docs): update package.json with new devDependencies

* feat(docs): migrate Integration Runtimes chapters

* feat(docs): update Database usage chapter

* feat(docs): restructure documentation paths

* chore(docs): clean up unused imports and files in documentation

* style(layout): revert navigation mode to previous state

* fix(docs): routing for documentation structure

* feat(openapi): add API documentation generation from OpenAPI schema

* feat(docs): add icons to documentation pages

* chore(dependencies): remove unused content-collections packages

* fix(types): fix type error for attachFile in source.ts

* feat(redirects): update root redirect destination to '/start'

* feat(search): add static search functionality

* chore(dependencies): update fumadocs-core and fumadocs-ui to latest versions

* feat(search): add Powered by Orama link

* feat(generate-openapi): add error handling for missing OpenAPI schema

* feat(scripts): add OpenAPI generation to build process

* feat(config): enable dynamic redirects and rewrites in development mode

* feat(layout): add GitHub token support for improved API rate limits

* feat(redirects): add 301 redirects for cloudflare pages

* feat(docs): add Vercel redirects configuration

* feat(config): enable standalone output for development environment

* chore(layout): adjust layout settings

* refactor(package): clean up ajv dependency versions

* feat(docs): add twoslash support

* refactor(layout): update DocsLayout import and navigation configuration

* chore(layout): clean up layout.tsx by commenting out GithubInfo

* fix(Search): add locale to search initialization

* chore(package): update fumadocs and orama to latest versions

* docs: add menu items descriptions

* feat(layout): add GitHub URL to the layout component

* feat(docs): add AutoTypeTable component to MDX components

* feat(app): implement AutoTypeTable rendering for AppEvents type

* docs(layout): switch callouts back to default components

* fix(config): use __filename and __dirname for module paths

* docs: add note about node.js 22 requirement

* feat(styles): add custom color variables for light and dark themes

* docs: add S3 setup instructions for media module

* docs: fix typos and indentation in media module docs

* docs: add local media adapter example for Node.js

* docs(media): add S3/R2 URL format examples and fix typo

* docs: add cross-links to initial config and seeding sections

* indent numbered lists content, clarified media serve locations

* fix mediacontroller tests

* feat(layout): add AnimatedGridPattern component for dynamic background

* style(layout): configure fancy ToC style ('clerk')

* fix(AnimatedGridPattern): correct strokeDasharray type

* docs: actualize docs

* feat: add favicon

* style(cloudflare): format code examples

* feat(layout): add Github and Discord footer icons

* feat(footer): add SVG social media icons for GitHub and Discord

* docs: adjusted auto type table, added llm functions

* added static deployment to cloudflare workers

* docs: change cf redirects to proxy *.mdx instead of redirecting

---------

Co-authored-by: dswbx <dennis.senn@gmx.ch>
Co-authored-by: cameronapak <cameronandrewpak@gmail.com>

* build: improve build script

* add missing exports, fix EntityTypescript imports

* media: Dropzone: add programmatic upload, additional events, loading state

* schema object: disable extended defaults to allow empty config values

* Feat/new docs deploy (#224)

* test

* try fixing pm

* try fixing pm

* fix docs on imports, export events correctly

---------

Co-authored-by: Tim Seriakov <59409712+timseriakov@users.noreply.github.com>
Co-authored-by: cameronapak <cameronandrewpak@gmail.com>
This commit is contained in:
dswbx
2025-08-01 15:55:59 +02:00
committed by GitHub
parent daaaae82b6
commit a298b65abf
430 changed files with 15041 additions and 12375 deletions
@@ -0,0 +1,206 @@
---
title: "Using the CLI"
description: "How to start a bknd instance using the CLI."
tags: ["documentation"]
---
The bknd package includes a command-line interface (CLI) that allows you to run a bknd instance and perform various tasks.
```
npx bknd
```
Here is the output:
```
$ npx bknd
Usage: bknd [options] [command]
⚡ bknd cli v0.16.0
Options:
-V, --version output the version number
-h, --help display help for command
Commands:
user <action> create/update users, or generate a token (auth)
types [options] generate types
schema [options] get schema
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
```
## Starting an instance (`run`)
To see all available `run` options, execute `npx bknd run --help`.
```
$ npx bknd run --help
Usage: bknd run [options]
Options:
-p, --port <port> port to run on (default: 1337, env: PORT)
-m, --memory use in-memory database
-c, --config <config> config file
--db-url <db> database url, can be any valid libsql url
--db-token <db> database token
--server <server> server type (choices: "node", "bun", default: "bun")
--no-open don't open browser window on start
-h, --help display help for command
```
To order in which the connection is determined is as follows:
1. `--db-url`
2. `--config` or reading the filesystem looking for `bknd.config.[js|ts|mjs|cjs|json]`
3. `--memory`
4. Environment variables `DB_URL` and `DB_TOKEN` in `.env` or `.dev.vars`
5. Fallback to file-based database `data.db`
### File-based database
By default, a file-based database `data.db` is used when running without any arguments. You can specify a different file name or path using the `--db-url` option. The database file will be created in the current working directory if it does not exist.
```
npx bknd run --db-url file:data.db
```
### Using configuration file (`bknd.config.*`)
You can create a configuration file on the working directory that automatically gets picked up: `bknd.config.[js|ts|mjs|cjs|json]`
Here is an example of a `bknd.config.ts` file:
```ts
import type { BkndConfig } from "bknd/adapter";
export default {
// you can either specify the connection directly
connection: {
url: "file:data.db",
},
// or use the `app` function which passes the environment variables
app: ({ env }) => ({
connection: {
url: env.DB_URL,
},
}),
} satisfies BkndConfig;
```
The `app` function is useful if you need a cross-platform way to access the environment variables. For example, on Cloudflare Workers, you can only access environment variables inside a request handler. If you're exclusively using a node-like environment, it's safe to access the environment variables directly from `process.env`.
If you're using `npx bknd run`, make sure to create a file in a file format that `node` can load, otherwise you may run into an error that the file couldn't be found:
```sh
[INF] 2025-03-28 18:02:21 Using config from bknd.config.ts
[ERR] 2025-03-28 18:02:21 Failed to load config: Error [ERR_MODULE_NOT_FOUND]: Cannot find package 'bknd.config.ts' imported from [...]
at packageResolve (node:internal/modules/esm/resolve:857:9)
at [...] {
code: 'ERR_MODULE_NOT_FOUND'
}
```
If you still want to use a `.ts` extension, you can start the CLI e.g. using `node` (>=v22.6.0):
```sh
node --experimental-strip-types node_modules/.bin/bknd run
```
Or with `tsx`:
```sh
npx tsx node_modules/.bin/bknd run
```
### Turso/LibSQL database
To start an instance with a Turso/LibSQL database, run the following:
```
npx bknd run --db-url libsql://your-db.turso.io --db-token <your-token>
```
The `--db-token` option is optional and only required if the database is protected.
### In-memory database
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.
## 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";
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" {
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`:
```typescript
import type { DB } from "bknd";
type Todo = DB["todos"];
```
All bknd methods that involve your database schema will be automatically typed.
@@ -0,0 +1,412 @@
---
title: "Database"
description: "Choosing the right database configuration"
tags: ["documentation"]
---
In order to use **bknd**, you need to prepare access information to your database and potentially install additional dependencies. Connections to the database are managed using Kysely. Therefore, all [its dialects](https://kysely.dev/docs/dialects) are theoretically supported.
Currently supported and tested databases are:
- SQLite (embedded): Node.js SQLite, Bun SQLite, LibSQL, SQLocal
- SQLite (remote): Turso, Cloudflare D1
- Postgres: Vanilla Postgres, Supabase, Neon, Xata
By default, bknd will try to use a SQLite database in-memory. Depending on your runtime, a different SQLite implementation will be used.
## Defining the connection
There are mainly 3 ways to define the connection to your database, when
1. creating an app using `App.create()` or `createApp()`
2. creating an app using a [Framework or Runtime adapter](/integration/introduction)
3. starting a quick instance using the [CLI](/usage/cli#using-configuration-file-bknd-config)
When creating an app using `App.create()` or `createApp()`, you can pass a connection object in the configuration object.
```typescript title="app.ts"
import { createApp } from "bknd";
import { sqlite } from "bknd/adapter/sqlite";
// a connection is required when creating an app like this
const app = createApp({
connection: sqlite({ url: ":memory:" }),
});
```
When using an adapter, or using the CLI, bknd will automatically try to use a SQLite implementation depending on the runtime:
```javascript title="app.js"
import { serve } from "bknd/adapter/node";
serve({
// connection is optional, but recommended
connection: { url: "file:data.db" },
});
```
You can also pass a connection instance to the `connection` property to explictly use a specific connection.
```javascript title="app.js"
import { serve } from "bknd/adapter/node";
import { sqlite } from "bknd/adapter/sqlite";
serve({
connection: sqlite({ url: "file:data.db" }),
});
```
If you're using [`bknd.config.*`](/extending/config), you can specify the connection on the exported object.
```typescript title="bknd.config.ts"
import type { BkndConfig } from "bknd";
export default {
connection: { url: "file:data.db" },
} as const satisfies BkndConfig;
```
Throughout the documentation, it is assumed you use `bknd.config.ts` to define your connection.
## SQLite
### Using config object
<Callout type="warn">
When run with Node.js, a version of 22 (LTS) or higher is required. Please
verify your version by running `node -v`, and
[upgrade](https://nodejs.org/en/download/) if necessary.
</Callout>
The `sqlite` adapter is automatically resolved based on the runtime.
| Runtime | Adapter | In-Memory | File | Remote |
| ------------------------------ | ------------- | --------- | ---- | ------ |
| Node.js | `node:sqlite` | ✅ | ✅ | ❌ |
| Bun | `bun:sqlite` | ✅ | ✅ | ❌ |
| Cloudflare Worker/Browser/Edge | `libsql` | 🟠 | 🟠 | ✅ |
The bundled version of the `libsql` connection only works with remote databases. However, you can pass in a `Client` from `@libsql/client`, see [LibSQL](#libsql) for more details.
```typescript title="bknd.config.ts"
import type { BkndConfig } from "bknd";
// no connection is required, bknd will use a SQLite database in-memory
// this does not work on edge environments!
export default {} as const satisfies BkndConfig;
// or explicitly in-memory
export default {
connection: { url: ":memory:" },
} as const satisfies BkndConfig;
// or explicitly as a file
export default {
connection: { url: "file:<path/to/your/database.db>" },
} as const satisfies BkndConfig;
```
### LibSQL
Turso offers a SQLite-fork called LibSQL that runs a server around your SQLite database. The edge-version of the adapter is included in the bundle (remote only):
```typescript title="bknd.config.ts"
import { libsql, type BkndConfig } from "bknd";
export default {
connection: libsql({
url: "libsql://<database>.turso.io",
authToken: "<auth-token>",
}),
} as const satisfies BkndConfig;
```
If you wish to use LibSQL as file, in-memory or make use of [Embedded Replicas](https://docs.turso.tech/features/embedded-replicas/introduction), you have to pass in the `Client` from `@libsql/client`:
```typescript title="bknd.config.ts"
import { libsql, type BkndConfig } from "bknd";
import { createClient } from "@libsql/client";
const client = createClient({
url: "libsql://<database>.turso.io",
authToken: "<auth-token>",
});
export default {
connection: libsql(client),
} as const satisfies BkndConfig;
```
### Cloudflare D1
Using the [Cloudflare Adapter](/integration/cloudflare), you can choose to use a D1 database binding. To do so, you only need to add a D1 database to your `wrangler.toml` and it'll pick up automatically.
To manually specify which D1 database to take, you can specify it explicitly:
```ts
import { serve, d1 } from "bknd/adapter/cloudflare";
export default serve<Env>({
app: ({ env }) => d1({ binding: env.D1_BINDING }),
});
```
### SQLocal
To use bknd with `sqlocal` for a offline expierence, you need to install the `@bknd/sqlocal` package. You can do so by running the following command:
```bash
npm install @bknd/sqlocal
```
This package uses `sqlocal` under the hood. Consult the [sqlocal documentation](https://sqlocal.dallashoffman.com/guide/setup) for connection options:
```js
import { createApp } from "bknd";
import { SQLocalConnection } from "@bknd/sqlocal";
const app = createApp({
connection: new SQLocalConnection({
databasePath: ":localStorage:",
verbose: true,
}),
});
```
## PostgreSQL
To use bknd with Postgres, you need to install the `@bknd/postgres` package. You can do so by running the following command:
```bash
npm install @bknd/postgres
```
You can connect to your Postgres database using `pg` or `postgres` dialects. Additionally, you may also define your custom connection.
### Using `pg`
To establish a connection to your database, you can use any connection options available on the [`pg`](https://node-postgres.com/apis/client) package.
```js
import { serve } from "bknd/adapter/node";
import { pg } from "@bknd/postgres";
/** @type {import("bknd/adapter/node").NodeBkndConfig} */
const config = {
connection: pg({
connectionString: "postgresql://user:password@localhost:5432/database",
}),
};
serve(config);
```
### Using `postgres`
To establish a connection to your database, you can use any connection options available on the [`postgres`](https://github.com/porsager/postgres) package.
```js
import { serve } from "bknd/adapter/node";
import { postgresJs } from "@bknd/postgres";
serve({
connection: postgresJs("postgresql://user:password@localhost:5432/database"),
});
```
### Using custom connection
Several Postgres hosting providers offer their own clients to connect to their database, e.g. suitable for serverless environments.
Example using `@neondatabase/serverless`:
```js
import { createCustomPostgresConnection } from "@bknd/postgres";
import { NeonDialect } from "kysely-neon";
const neon = createCustomPostgresConnection(NeonDialect);
serve({
connection: neon({
connectionString: process.env.NEON,
}),
});
```
Example using `@xata.io/client`:
```js
import { createCustomPostgresConnection } from "@bknd/postgres";
import { XataDialect } from "@xata.io/kysely";
import { buildClient } from "@xata.io/client";
const client = buildClient();
const xata = new client({
databaseURL: process.env.XATA_URL,
apiKey: process.env.XATA_API_KEY,
branch: process.env.XATA_BRANCH,
});
const xataConnection = createCustomPostgresConnection(XataDialect, {
supports: {
batching: false,
},
});
serve({
connection: xataConnection({ xata }),
});
```
## Custom Connection
Creating a custom connection is as easy as extending the `Connection` class and passing constructing a Kysely instance.
```ts
import { createApp, Connection } from "bknd";
import { Kysely } from "kysely";
class CustomConnection extends Connection {
constructor() {
const kysely = new Kysely(/* ... */);
super(kysely);
}
}
const connection = new CustomConnection();
// e.g. and then, create an instance
const app = createApp({ connection });
```
## Initial Structure
To provide an initial database structure, you can pass `initialConfig` to the creation of an app. This will only be used if there isn't an existing configuration found in the database given. Here is a quick example:
<Callout type="info">
The initial structure is only respected if the database is empty! If you made
updates, ensure to delete the database first, or perform updates through the
Admin UI.
</Callout>
```typescript
import { createApp, em, entity, text, number } from "bknd";
const schema = em(
{
posts: entity("posts", {
// "id" is automatically added
title: text().required(),
slug: text().required(),
content: text(),
views: number(),
}),
comments: entity("comments", {
content: text(),
}),
// relations and indices are defined separately.
// the first argument are the helper functions, the second the entities.
},
({ relation, index }, { posts, comments }) => {
relation(comments).manyToOne(posts);
// relation as well as index can be chained!
index(posts).on(["title"]).on(["slug"], true);
},
);
// to get a type from your schema, use:
type Database = (typeof schema)["DB"];
// type Database = {
// posts: {
// id: number;
// title: string;
// content: string;
// views: number;
// },
// comments: {
// id: number;
// content: string;
// }
// }
// pass the schema to the app
const app = createApp({
connection: {
/* ... */
},
initialConfig: {
data: schema.toJSON(),
},
});
```
Note that we didn't add relational fields directly to the entity, but instead defined them afterwards. That is because the relations are managed outside the entity scope to have an unified expierence for all kinds of relations (e.g. many-to-many).
<Callout type="info">
Defined relations are currently not part of the produced types for the
structure. We're working on that, but in the meantime, you can define them
manually.
</Callout>
### Type completion
To get type completion, there are two options:
1. Use the CLI to [generate the types](/usage/cli#generating-types-types)
2. If you have an initial structure created with the prototype functions, you can extend the `DB` interface with your own schema.
All entity related functions use the types defined in `DB` from `bknd`. To get type completion, you can extend that interface with your own schema:
```typescript
import { em } from "bknd";
import { Api } from "bknd/client";
const schema = em({
/* ... */
});
type Database = (typeof schema)["DB"];
declare module "bknd" {
interface DB extends Database {}
}
const api = new Api({
/* ... */
});
const { data: posts } = await api.data.readMany("posts", {});
// `posts` is now typed as Database["posts"]
```
The type completion is available for the API as well as all provided [React hooks](/usage/react).
### Seeding the database
To seed your database with initial data, you can pass a `seed` function to the configuration. It
provides the `ModuleBuildContext` as the first argument.
<Callout type="info">
Note that the seed function will only be executed on app's first boot. If a
configuration already exists in the database, it will not be executed.
</Callout>
```typescript
import { createApp, type ModuleBuildContext } from "bknd";
const app = createApp({
connection: {
/* ... */
},
initialConfig: {
/* ... */
},
options: {
seed: async (ctx: ModuleBuildContext) => {
await ctx.em.mutator("posts").insertMany([
{ title: "First post", slug: "first-post", content: "..." },
{ title: "Second post", slug: "second-post" },
]);
},
},
});
```
@@ -0,0 +1,151 @@
---
title: "React Elements"
description: "Speed up your frontend development"
tags: ["documentation"]
---
Not only creating and maintaing a backend is time-consuming, but also integrating it into your frontend can be a hassle. With `bknd/elements`, you can easily add media uploads and authentication forms to your app without having to figure out API details.
<Callout type="info">
In order to use these exported elements, make sure to wrap your app inside
`ClientProvider`. See the [React Setup](/usage/react#setup) for more
information.
</Callout>
## Media
### Media.Dropzone
The `Media.Dropzone` element allows retrieving from and uploading media items to your bknd instance. Without any properties specified, it will behave similar to your media library inside the bknd Admin UI. Here is how to get the last 10 items:
```tsx
import { Media } from "bknd/elements";
export default function MediaGallery() {
return <Media.Dropzone query={{ limit: 10, sort: "-id" }} />;
}
```
Since you can also upload media to a specific entity, you can also point that `Dropzone` to it. Here is an example of a single user avatar that gets overwritten on re-upload:
```tsx
import { Media } from "bknd/elements";
export default function UserAvatar() {
return (
<Media.Dropzone
entity={{ name: "users", id: 1, field: "avatar" }}
maxItems={1}
overwrite
/>
);
}
```
#### Props
- `initialItems?: xMediaFieldSchema[]`: Initial items to display, must be an array of media objects.
- `entity?: { name: string; id: number; field: string }`: If given, the initial media items fetched will be from this entity.
- `query?: RepoQueryIn`: Query to filter the media items.
- `overwrite?: boolean`: If true, the media item will be overwritten on entity media uploads if limit was reached.
- `maxItems?: number`: Maximum number of media items that can be uploaded.
- `autoUpload?: boolean`: If true, the media items will be uploaded automatically.
- `onRejected?: (files: FileWithPath[]) => void`: Callback when a file is rejected.
- `onDeleted?: (file: FileState) => void`: Callback when a file is deleted.
- `onUploaded?: (file: FileState) => void`: Callback when a file is uploaded.
- `placeholder?: { show?: boolean; text?: string }`: Placeholder text to show when no media items are present.
#### Customize Rendering
You can also customize the rendering of the media items and its uploading by passing a react element as a child. Here is an example of a custom `Media.Dropzone` that renders an user avatar (styled using tailwind):
```tsx
import { Media, useMediaDropzone, useMediaDropzoneState } from "bknd/elements";
export default function CustomUserAvatar() {
return (
<Media.Dropzone
entity={{ name: "users", id: 1, field: "avatar" }}
maxItems={1}
overwrite
>
<CustomUserAvatar />
</Media.Dropzone>
);
}
function CustomUserAvatar() {
const {
wrapperRef,
inputProps,
showPlaceholder,
actions: { openFileInput },
} = useMediaDropzone();
const {
files: [file],
isOver,
isOverAccepted,
} = useMediaDropzoneState();
return (
<div
ref={wrapperRef}
className="size-32 rounded-full border border-gray-200 flex justify-center items-center leading-none overflow-hidden"
>
<div className="hidden">
<input {...inputProps} />
</div>
{showPlaceholder && (
<>{isOver && isOverAccepted ? "let it drop" : "drop here"}</>
)}
{file && (
<Media.Preview
file={file}
className="object-cover w-full h-full"
onClick={openFileInput}
/>
)}
</div>
);
}
```
## Auth
Adding authentication to your app with bknd is as easy as adding a `<form method="POST" />` with an action pointing to the action (`login` or `register`) to the strategy you want to use, e.g. for the password strategy, use `/api/auth/password/login`. But to make it even easier, you can use the `Auth.*` elements.
### `Auth.Screen`
The `Auth.Screen` element is a wrapper around the `Auth.Form` element that provides a full page screen. The current layout is admittedly very basic, but there will be more customization options in the future.
```tsx
import { Auth } from "bknd/elements";
export default function LoginScreen() {
return <Auth.Screen action="login" />;
}
```
#### Props
Note that this component doesn't require any strategy-specific information, as it gathers it itself.
- `action: "login" | "register"`: The action to perform.
- `method?: "POST" | "GET"`: The method to use for the form.
### `Auth.Form`
If you only wish to render the form itself without the screen, you can use the `Auth.Form` element. Unlike the `Auth.Screen`, this element requires the `strategy` prop to be set to the strategy you want to use. You can either specify it manually, use use the exported hook `useAuthStrategies()` for fetch them from your bknd instance.
```tsx
import { Auth, useAuthStrategies } from "bknd/elements";
export default function LoginForm() {
const { strategies, basepath, loading } = useAuthStrategies();
if (loading) return null;
return (
<Auth.Form action="login" strategies={strategies} basepath={basepath} />
);
}
```
@@ -0,0 +1,228 @@
---
title: "Introduction"
description: "Setting up bknd"
tags: ["documentation"]
---
There are several methods to get **bknd** up and running. You can choose between these options:
1. [Run it using the CLI](/usage/cli): That's the easiest and fastest way to get started.
2. Use a runtime like [Node](/integration/node), [Bun](/integration/bun) or
[Cloudflare](/integration/cloudflare) (workerd). This will run the API and UI in the runtime's
native server and serves the UI assets statically from `node_modules`.
3. Run it inside your React framework of choice like [Next.js](/integration/nextjs),
[Astro](/integration/astro) or [Remix](/integration/remix).
There is also a fourth option, which is running it inside a
[Docker container](/integration/docker). This is essentially a wrapper around the CLI.
## Basic setup
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:
```typescript
import { createApp, type CreateAppConfig } from "bknd";
// create the app
const config = {
/* ... */
} satisfies CreateAppConfig;
const app = createApp(config);
// build the app
await app.build();
// export for Web API compliant envs
export default app;
```
In Web API compliant environments, all you have to do is to default exporting the app, as it
implements the `Fetch` API.
## Configuration (`CreateAppConfig`)
The `CreateAppConfig` type is the main configuration object for the `createApp` function. It has
the following properties:
```typescript
import type { App, InitialModuleConfigs, ModuleBuildContext, Connection } from "bknd";
import type { Config } from "@libsql/client";
type AppPlugin = (app: App) => Promise<void> | void;
type ManagerOptions = {
basePath?: string;
trustFetched?: boolean;
onFirstBoot?: () => Promise<void>;
seed?: (ctx: ModuleBuildContext) => Promise<void>;
};
type CreateAppConfig = {
connection?: Connection | Config;
initialConfig?: InitialModuleConfigs;
options?: {
plugins?: AppPlugin[];
manager?: ManagerOptions;
};
};
```
### `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
const connection = {
url: "<url>",
authToken: "<token>",
};
```
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.
### `initialConfig`
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 to get the default configuration:
```sh
npx bknd config --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` 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 },
]);
};
```
### `options.manager`
This object is passed to the `ModuleManager` which is responsible for:
- validating and maintaining configuration of all modules
- building all modules (data, auth, media, flows)
- maintaining the `ModuleBuildContext` used by the modules
The `options.manager` object has the following properties:
- `basePath` (`string`): The base path for the Hono instance. This is used to prefix all routes.
- `trustFetched` (`boolean`): If set to `true`, the app will not perform any validity checks for
the given or fetched configuration.
- `onFirstBoot` (`() => Promise<void>`): A function that is called when the app is booted for
the first time.
@@ -0,0 +1,240 @@
---
title: "SDK (React)"
description: "Use the bknd SDK for React"
tags: ["documentation"]
---
There are 4 useful hooks to work with your backend:
1. simple hooks which are solely based on the [API](/usage/sdk):
- [`useApi`](#useapi)
- [`useEntity`](#useentity)
2. query hooks that wraps the API in [SWR](https://swr.vercel.app/):
- [`useApiQuery`](#useapiquery)
- [`useEntityQuery`](#useentityquery)
## Setup
In order to use them, make sure you wrap your `<App />` inside `<ClientProvider />`, so that these hooks point to your bknd instance:
```tsx
import { ClientProvider } from "bknd/client";
export default function App() {
return <ClientProvider>{/* your app */}</ClientProvider>;
}
```
For all other examples below, we'll assume that your app is wrapped inside the `ClientProvider`.
## `useApi()`
To use the simple hook that returns the Api, you can use:
```tsx
import { useApi } from "bknd/client";
export default function App() {
const api = useApi();
// ...
}
```
## `useApiQuery()`
This hook wraps the API class in an SWR hook for convenience. You can use any API endpoint
supported, like so:
```tsx
import { useApiQuery } from "bknd/client";
export default function App() {
const { data, ...swr } = useApiQuery((api) => api.data.readMany("comments"));
if (swr.error) return <div>Error</div>;
if (swr.isLoading) return <div>Loading...</div>;
return <pre>{JSON.stringify(data, null, 2)}</pre>;
}
```
### Props
- `selector: (api: Api) => FetchPromise`
The first parameter is a selector function that provides an Api instance and expects an
endpoint function to be returned.
- `options`: optional object that inherits from `SWRConfiguration`
```ts
type Options<Data> = import("swr").SWRConfiguration & {
enabled?: boolean;
refine?: (data: Data) => Data | any;
};
```
* `enabled`: Determines whether this hook should trigger a fetch of the data or not.
* `refine`: Optional refinement that is called after a response from the API has been
received. Useful to omit irrelevant data from the response (see example below).
### Using mutations
To query and mutate data using this hook, you can leverage the parameters returned. In the
following example we'll also use a `refine` function as well as `revalidateOnFocus` (option from
`SWRConfiguration`) so that our data keeps updating on window focus change.
```tsx
import { useEffect, useState } from "react";
import { useApiQuery } from "bknd/client";
export default function App() {
const [text, setText] = useState("");
const { data, api, mutate, ...q } = useApiQuery(
(api) => api.data.readOne("comments", 1),
{
// filter to a subset of the response
refine: (data) => data.data,
revalidateOnFocus: true,
},
);
const comment = data ? data : null;
useEffect(() => {
setText(comment?.content ?? "");
}, [comment]);
if (q.error) return <div>Error</div>;
if (q.isLoading) return <div>Loading...</div>;
return (
<form
onSubmit={async (e) => {
e.preventDefault();
if (!comment) return;
// this will automatically revalidate the query
await mutate(async () => {
const res = await api.data.updateOne("comments", comment.id, {
content: text,
});
return res.data;
});
return false;
}}
>
<input
type="text"
value={text}
onChange={(e) => setText(e.target.value)}
/>
<button type="submit">Update</button>
</form>
);
}
```
## `useEntity()`
This hook wraps the endpoints of `DataApi` and returns CRUD options as parameters:
```tsx
import { useState, useEffect } from "react";
import { useEntity } from "bknd/client";
export default function App() {
const [data, setData] = useState<any>();
const { create, read, update, _delete } = useEntity("comments", 1);
useEffect(() => {
read().then(setData);
}, []);
return <pre>{JSON.stringify(data, null, 2)}</pre>;
}
```
If you only supply the entity name as string without an ID, the `read` method will fetch a list
of entities instead of a single entry.
### Props
Following props are available when using `useEntityQuery([entity], [id?])`:
- `entity: string`: Specify the table name of the entity
- `id?: number | string`: If an id given, it will fetch a single entry, otherwise a list
### Returned actions
The following actions are returned from this hook:
- `create: (input: object)`: Create a new entry
- `read: (query: Partial<RepoQuery> = {})`: If an id was given,
it returns a single item, otherwise a list
- `update: (input: object, id?: number | string)`: If an id was given, the id parameter is
optional. Updates the given entry partially.
- `_delete: (id?: number | string)`: If an id was given, the id parameter is
optional. Deletes the given entry.
## `useEntityQuery()`
This hook wraps the actions from `useEntity` around `SWR`. The previous example would look like
this:
```tsx
import { useEntityQuery } from "bknd/client";
export default function App() {
const { data } = useEntityQuery("comments", 1);
return <pre>{JSON.stringify(data, null, 2)}</pre>;
}
```
### Using mutations
All actions returned from `useEntityQuery` are conveniently wrapped around the `mutate` function,
so you don't have think about this:
```tsx
import { useState, useEffect } from "react";
import { useEntityQuery } from "bknd/client";
export default function App() {
const [text, setText] = useState("");
const { data, update, ...q } = useEntityQuery("comments", 1);
const comment = data ? data : null;
useEffect(() => {
setText(comment?.content ?? "");
}, [comment]);
if (q.error) return <div>Error</div>;
if (q.isLoading) return <div>Loading...</div>;
return (
<form
onSubmit={async (e) => {
e.preventDefault();
if (!comment) return;
// this will automatically revalidate the query
await update({ content: text });
return false;
}}
>
<input
type="text"
value={text}
onChange={(e) => setText(e.target.value)}
/>
<button type="submit">Update</button>
</form>
);
}
```
@@ -0,0 +1,243 @@
---
title: "SDK (TypeScript)"
description: "Use the bknd SDK in TypeScript"
tags: ["documentation"]
---
To start using the bknd API, start by creating a new API instance:
```ts
import { Api } from "bknd/client";
const api = new Api();
// always make sure to verify auth
await api.verifyAuth();
```
The `Api` class is the main entry point for interacting with the bknd API. It provides methods
for all available modules described below.
## Setup
You can initialize an API instance by providing the `Request` object, or manually specifying the details such as `host` and `token`.
### Using the `Request` object
The recommended way to create an API instance is by passing the current `Request` object. This will automatically point the API to your current instance and extract the token from the headers (either from cookies or `Authorization` header):
```ts
import { Api } from "bknd/client";
// replace this with the actual request
let request: Request;
const api = new Api({ request });
```
If the authentication details are contained in the current request, but you're hosting your bknd instance somewhere else, you can specify a `host` option:
```ts
import { Api } from "bknd/client";
// replace this with the actual request
let request: Request;
const api = new Api({
host: "https://<your-endpoint>",
request,
});
```
### Using the `token` option
If you want to have an API instance that is using a different token, e.g. an admin token, you can create it by specifying the `host` and `token` option:
```ts
import { Api } from "bknd/client";
const api = new Api({
host: "https://<your-endpoint>",
token: "<your-token>",
});
```
### Using a local API
In case the place where you're using the API is the same as your bknd instance (e.g. when using it embedded in a React framework), you can specify a `fetcher` option to point to your bknd app. This way, requests won't travel over the network and instead processed locally:
```ts
import type { App } from "bknd";
import { Api } from "bknd/client";
// replace this with your actual `App` instance
let app: App;
const api = new Api({
fetcher: app.server.request as typeof fetch,
// specify `host` and `token` or `request`
});
```
## Data (`api.data`)
Access the `Data` specific API methods at `api.data`.
### `data.readMany([entity], [query])`
To retrieve a list of records from an entity, use the `readMany` method:
```ts
const { data } = await api.data.readMany("posts");
```
You can also add additional query instructions:
```ts
const { data } = await api.data.readMany("posts", {
limit: 10,
offset: 0,
select: ["id", "title", "views"],
with: {
// join last 2 comments
comments: {
with: {
// along with the comments' user
users: {},
},
limit: 2,
sort: "-id",
},
// also get the first 2 images, but only the path
images: {
select: ["path"],
limit: 2,
},
},
where: {
// same as '{ title: { $eg: "Hello, World!" } }'
title: "Hello, World!",
// only with views greater than 100
views: {
$gt: 100,
},
},
// sort by views descending (without "-" would be ascending)
sort: "-views",
});
```
The `with` property automatically adds the related entries to the response.
### `data.readOne([entity], [id])`
To retrieve a single record from an entity, use the `readOne` method:
```ts
const { data } = await api.data.readOne("posts", 1);
```
### `data.createOne([entity], [data])`
To create a single record of an entity, use the `createOne` method:
```ts
const { data } = await api.data.createOne("posts", {
title: "Hello, World!",
content: "This is a test post.",
views: 0,
});
```
### `data.createMany([entity], [data])`
To create many records of an entity, use the `createMany` method:
```ts
const { data } = await api.data.createMany("posts", [
{ title: "Hello, World!" },
{ title: "Again, Hello." },
]);
```
### `data.updateOne([entity], [id], [data])`
To update a single record of an entity, use the `updateOne` method:
```ts
const { data } = await api.data.updateOne("posts", 1, {
views: 1,
});
```
### `data.updateMany([entity], [where], [update])`
To update many records of an entity, use the `updateMany` method:
```ts
const { data } = await api.data.updateMany(
"posts",
{ views: { $gt: 1 } },
{
title: "viewed more than once",
},
);
```
### `data.deleteOne([entity], [id])`
To delete a single record of an entity, use the `deleteOne` method:
```ts
const { data } = await api.data.deleteOne("posts", 1);
```
### `data.deleteMany([entity], [where])`
To delete many records of an entity, use the `deleteMany` method:
```ts
const { data } = await api.data.deleteMany("posts", { views: { $lte: 1 } });
```
## Auth (`api.auth`)
Access the `Auth` specific API methods at `api.auth`. If there is successful authentication, the
API will automatically save the token and use it for subsequent requests.
### `auth.strategies()`
To retrieve the available authentication strategies, use the `strategies` method:
```ts
const { data } = await api.auth.strategies();
```
### `auth.login([strategy], [input])`
To log in with a password, use the `login` method:
```ts
const { data } = await api.auth.login("password", {
email: "...",
password: "...",
});
```
### `auth.register([strategy], [input])`
To register with a password, use the `register` method:
```ts
const { data } = await api.auth.register("password", {
email: "...",
password: "...",
});
```
### `auth.me()`
To retrieve the current user, use the `me` method:
```ts
const { data } = await api.auth.me();
```