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 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.
When creating an app using App.create() or createApp(), you can pass a connection object in the configuration object.
app.ts
import { createApp } from "bknd";import { sqlite } from "bknd/adapter/sqlite";// a connection is required when creating an app like thisconst 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:
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.
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.*, you can specify the connection on the exported object.
bknd.config.ts
import type { BkndConfig } from "bknd";export default { connection: { url: "file:data.db" },} satisfies BkndConfig;
Throughout the documentation, it is assumed you use bknd.config.ts to define your connection.
When run with Node.js, a version of 22 (LTS) or higher is required. Please
verify your version by running node -v, and
upgrade if necessary.
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 for more details.
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 {} satisfies BkndConfig;// or explicitly in-memoryexport default { connection: { url: ":memory:" },} satisfies BkndConfig;// or explicitly as a fileexport default { connection: { url: "file:<path/to/your/database.db>" },} satisfies BkndConfig;
To use the Node.js SQLite adapter directly, use the nodeSqlite function as value for the connection property. This lets you customize the database connection, such as enabling WAL mode.
You can explicitly use the Bun SQLite adapter by passing the bunSqlite function to the connection property. This allows further configuration of the database, e.g. enabling WAL mode.
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):
bknd.config.ts
import { libsql, type BkndConfig } from "bknd";export default { connection: libsql({ url: "libsql://<database>.turso.io", authToken: "<auth-token>", }),} satisfies BkndConfig;
If you wish to use LibSQL as file, in-memory or make use of Embedded Replicas, you have to pass in the Client from @libsql/client:
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),} satisfies BkndConfig;
Using the Cloudflare Adapter, 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:
To provide a database structure, you can pass config to the creation of an app. In db mode, the data 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.
Here is a quick example:
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 appconst app = createApp({ config: { 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).
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.
There are multiple system entities which are added depending on if the module is enabled:
users: if authentication is enabled
media: if media is enabled and an adapter is configured
You can add additional fields to these entities. System-defined fields don't have to be repeated, those are automatically added to the entity, so don't worry about that. It's important though to match the system entities name, otherwise a new unrelated entity will be created.
If you'd like to connect your entities to system entities, you need them in the schema to access their reference when making relations. From the example above, if you'd like to connect the posts entity to the users entity, you can do so like this:
import { em, entity, text, number, systemEntity } from "bknd";const schema = em( { posts: entity("posts", { title: text().required(), slug: text().required(), content: text(), views: number(), // don't add the foreign key field, it's automatically added }), comments: entity("comments", { content: text(), }), // add a `users` entity users: systemEntity("users", { // optionally add additional fields }) }, // now you have access to the system entity "users" ({ relation, index }, { posts, comments, users }) => { // ... other relations relation(posts).manyToOne(users); },);
If media is enabled, you can upload media directly or associate it with an entity. E.g. you may want to upload a cover image for a post, but also a gallery of images. Since a relation to the media entity is polymorphic, you have to:
add a virtual field to your entity (single medium or multiple media)
add the relation from the owning entity to the media entity
specify the mapped field name by using the mappedBy option
import { em, entity, text, number, systemEntity, medium, media } from "bknd";const schema = em( { posts: entity("posts", { title: text().required(), slug: text().required(), content: text(), views: number(), // `medium` represents a single media item cover: medium(), // `media` represents a list of media items gallery: media(), }), comments: entity("comments", { content: text(), }), // add the `media` entity media: systemEntity("media", { // optionally add additional fields }) }, // now you have access to the system entity "media" ({ relation, index }, { posts, comments, media }) => { // add the `cover` relation relation(posts).polyToOne(media, { mappedBy: "cover" }); // add the `gallery` relation relation(posts).polyToMany(media, { mappedBy: "gallery" }); },);
Note that in db mode, 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.
In code mode, the seed function will not be automatically executed. You can manually execute it by running the following command:
npx bknd sync --seed --force
See the sync command documentation for more details.