In order to use bknd, you need to prepare access information to your database and install the dependencies.

Connections to the database are managed using Kysely. Therefore, all its dialects are theoretically supported. However, only the SQLite dialect is implemented as of now.

Database

SQLite as file

The easiest to get started is using SQLite as a file. When serving the API in the “Integrations”, the function accepts an object with connection details. To use a file, use the following:

{
   "type": "libsql",
   "config": {
      "url": "file:<path/to/your/database.db>"
   }
}

Please note that using SQLite as a file is only supported in server environments.

SQLite using LibSQL

Turso offers a SQLite-fork called LibSQL that runs a server around your SQLite database. To point bknd to a local instance of LibSQL, install Turso’s CLI and run the following command:

turso dev

The command will yield a URL. Use it in the connection object:

{
   "type": "libsql",
   "config": {
      "url": "http://localhost:8080"
   }
}

SQLite using LibSQL on Turso

If you want to use LibSQL on Turso, sign up for a free account, create a database and point your connection object to your new database:

{
   "type": "libsql",
   "config": {
      "url": "libsql://your-database-url.turso.io",
      "authToken": "your-auth-token"
   }
}

Custom Connection

Follow the progress of custom connections on its Github Issue. If you’re interested, make sure to upvote so it can be prioritized.

Any bknd app instantiation accepts as connection either undefined, a connection object like described above, or an class instance that extends from Connection:

import { createApp } from "bknd";
import { Connection } from "bknd/data";

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:

import { em, entity, text, number } from "bknd/data";

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

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.

Type completion

All entity related functions use the types defined in DB from bknd/core. To get type completion, you can extend that interface with your own schema:

import { em } from "bknd/data";
import { Api } from "bknd";

// const schema = em({ ... });

type Database = (typeof schema)["DB"];
declare module "bknd/core" {
   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.

Seeding the database

To seed your database with initial data, you can pass a seed function to the configuration. It provides the ModuleBuildContext (reference) as the first argument.

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.

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" }
         ]);
      }
   }
});