Compare commits

..

48 Commits

Author SHA1 Message Date
Cameron Pak 99c783e7f0 rm plan doc 2026-03-07 10:34:01 -06:00
Cameron Pak 4defc4d1e7 refactor(cli): use errore for typed error handling in run command
Replace untyped catch blocks and scattered process.exit() with
errore tagged errors and error-as-values pattern. Compile-time
safety for error handling paths.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 10:30:21 -06:00
Cameron Pak fd1323fecc Add simplified command to build bknd 2026-03-07 10:05:15 -06:00
Cameron Pak 44d7c092f8 fix(cli): detect bun: protocol imports and re-exec under Bun
Catch ERR_UNSUPPORTED_ESM_URL_SCHEME with bun: protocol in addition
to ReferenceError "Bun is not defined" when deciding to re-exec.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 10:02:16 -06:00
Cameron Pak 45cf746855 fix(cli): refine type-stripping version check and add build:all script
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 10:02:11 -06:00
Cameron Pak db961b5447 fix(cli): replace jiti with Node native type-stripping for TS configs
Drop jiti dependency. Node >=22.18 imports .ts natively; 22.13–22.17 re-execs with --experimental-strip-types. Bun re-exec unchanged.
2026-03-07 09:50:56 -06:00
Cameron Pak 48d432ddb2 fix(cli): add re-exec loop guard and robust Bun error matching
Prevent infinite re-exec loop when Bun is installed but config still
fails. Use regex for error matching instead of exact string comparison.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 08:55:37 -06:00
Cameron Pak 980886b5c5 fix(cli): fix TS2345 for process.argv[1] type narrowing
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 08:48:16 -06:00
Cameron Pak 827e7faa7f fix(cli): support loading bknd.config.ts and detect runtime mismatch
Add jiti to handle TypeScript config imports natively in the CLI, removing the need for npx tsx or --experimental-strip-types workarounds. Detect Bun adapter usage under Node and suggest running CLI with the correct runtime.

Closes #360
2026-03-07 08:41:04 -06:00
Cameron Pak e6b17f6424 fix(cli): support loading bknd.config.ts without tsx workaround
Add jiti as a dependency to handle TypeScript config imports natively in the CLI, removing the need for npx tsx or --experimental-strip-types workarounds.

Closes #360
2026-03-07 08:06:51 -06:00
Cameron Pak 7832328a50 Merge pull request #359 from jonaspm/patch-1
Update client:only attribute in astro.mdx
2026-03-03 11:37:59 -06:00
Jonás Perusquía Morales e5bfc602e1 Update client:only attribute in astro.mdx
Astro requires client:only to specify the frontend library used
2026-03-03 11:27:59 -06:00
Cameron Pak 3894271b17 Merge pull request #353 from jonaspm/docs/auth-change-password
docs: Document changePassword method in auth module
2026-02-28 11:38:33 -06:00
Jonas Perusquia Morales cc1e1976f7 chore: Document error cases for password update method 2026-02-23 13:03:37 -06:00
dswbx 224d98879a Merge pull request #356 from jonaspm/fix/cross-platform-cleanup
Make dist clean cross-platform without shell commands
2026-02-20 18:00:46 +01:00
Jonas Perusquia Morales d1522c97ea Make dist clean cross-platform without shell commands 2026-02-20 09:50:30 -06:00
Jonas Perusquia Morales 8805f017a1 Update auth docs to clarify server module usage 2026-02-18 11:12:14 -06:00
Jonas Perusquia Morales 0ef06375c0 docs: Document changePassword method in auth module 2026-02-18 11:03:58 -06:00
dswbx 3d5c2cd3a8 Merge pull request #350 from xretic/main
refactor adapter tests for improved structure
2026-02-11 10:02:56 +01:00
roma fc60dfd042 refactor adapter tests for improved structure
reduced test duplication by extracting a small helper to strip connection from configs
2026-02-09 03:07:58 +01:00
Cameron Pak c8ee30b3da Merge pull request #348 from bknd-io/intro-docs-updates
Docs: consolidate dual introductions, restructure setup page
2026-02-07 17:23:21 -06:00
Cameron Pak b85c3a8a9b Remove redundant redirects config 2026-02-07 17:17:36 -06:00
Cameron Pak e20c2ef92c Docs: move redirects to global config, remove client-side redirect page
Co-Authored-By: Craft Agent <agents-noreply@craft.do>
2026-02-07 17:14:11 -06:00
Cameron Pak 5873baad3c Docs: add redirect from /usage/introduction to /usage/setup
Co-Authored-By: Craft Agent <agents-noreply@craft.do>
2026-02-07 17:09:36 -06:00
Cameron Pak aca162f9c8 Refactor setup documentation for clarity 2026-02-07 16:58:49 -06:00
Cameron Pak 9f0997c28a Refactor and update documentation links for introduction 2026-02-07 16:50:51 -06:00
dswbx 68063986d1 Merge pull request #343 from cameronapak/doc-touchups 2026-01-31 11:22:25 +01:00
Cameron Pak 53f8609e9d Docs: updating copy to reflect bknd's open opt-in philosophy 2026-01-30 03:39:53 -06:00
Cameron Pak 833bfa5049 Docs: Enhance the pun
https://github.com/bknd-io/bknd/pull/343#discussion_r2743055999
2026-01-30 03:28:12 -06:00
dswbx defcdfef0b Merge pull request #342 from cameronapak/guard-config
Docs: Guard lockout and how to set up Guard properly
2026-01-29 19:39:42 +01:00
cameronapak 348ac7f8b0 Docs: Refactor start page for clarity and flow 2026-01-25 15:20:20 -06:00
cameronapak 16b1cde8ea Docs: Add beta warning callout to start page 2026-01-25 13:58:43 -06:00
cameronapak 436b195242 Docs: Add warning about Guard lockout
Adds a callout to the `config.mdx` documentation warning users that
enabling the Guard without first creating an admin user will lock them
out.
2026-01-25 13:30:31 -06:00
dswbx ccd504440e Merge pull request #340 from bknd-io/chore/license
update license to Apache-2.0 in package.json
2026-01-21 13:30:02 +01:00
dswbx 023f5391e4 chore: update license to Apache-2.0 in package.json 2026-01-21 11:02:18 +01:00
dswbx 9685c5d4e1 Merge pull request #337 from jonaspm/chore/upgrade-astro-version
chore: Update Astro to version 5.16.9 in example package
2026-01-15 14:25:20 +01:00
Jonas Perusquia Morales 9b008be512 chore: Update Astro to version 5.16.9 in example package 2026-01-14 12:52:38 -06:00
dswbx 5352331464 Merge pull request #327 from matschik/main
docs(cloudflare): fix import in Cloudflare configuration example
2026-01-11 19:32:50 +01:00
dswbx 0e4f25cf11 refactor: update type handling in SvelteKit adapter tests and MediaApi uploadFile method 2026-01-09 14:51:42 +01:00
dswbx 4a5bdade8d fix: handle lazy import errors for TestRoutes in development for nextjs 2026-01-09 14:42:18 +01:00
dswbx 047bb80a97 Merge pull request #330 from jonaspm/fix/upload-media-entity-overwrite
[WIP] fix: Add overwrite option to uploadToEntity method
2026-01-09 14:34:41 +01:00
dswbx d826cb23a3 Merge pull request #291 from bknd-io/release/0.20
Release 0.20
2026-01-09 14:32:01 +01:00
dswbx f9373bd0e6 test: remove only 2026-01-09 11:30:04 +01:00
dswbx 5384738b67 tests: ensuring overwrite is passed and respected in media api 2026-01-09 11:28:19 +01:00
dswbx d82e28e6b1 Merge remote-tracking branch 'origin/release/0.20' into fork/jonaspm/fix/upload-media-entity-overwrite 2026-01-09 10:12:21 +01:00
Jonas Perusquia Morales c38bc95b1d fix: Add query support to media upload API methods 2026-01-08 12:37:13 -06:00
Mathieu Schimmerling 6651fce099 Merge pull request #1 from matschik/matschik-patch-1
docs(cloudflare): modify import in Cloudflare configuration example
2026-01-02 19:38:19 +01:00
Mathieu Schimmerling 91a1fc8a0c docs(cloudflare): modify import in Cloudflare configuration example
Updated import statement to include 'd1' from 'bknd/adapter/cloudflare'.
2026-01-02 19:13:24 +01:00
26 changed files with 718 additions and 415 deletions
+57 -47
View File
@@ -1,66 +1,76 @@
import { expect, describe, it, beforeAll, afterAll, mock } from "bun:test";
import * as adapter from "adapter";
import { disableConsoleLog, enableConsoleLog } from "core/utils";
import { disableConsoleLog, enableConsoleLog, omitKeys } from "core/utils";
import { adapterTestSuite } from "adapter/adapter-test-suite";
import { bunTestRunner } from "adapter/bun/test";
import { omitKeys } from "core/utils";
const stripConnection = <T extends Record<string, any>>(cfg: T) =>
omitKeys(cfg, ["connection"]);
beforeAll(disableConsoleLog);
afterAll(enableConsoleLog);
describe("adapter", () => {
it("makes config", async () => {
expect(omitKeys(await adapter.makeConfig({}), ["connection"])).toEqual({});
expect(
omitKeys(await adapter.makeConfig({}, { env: { TEST: "test" } }), ["connection"]),
).toEqual({});
describe("makeConfig", () => {
it("returns empty config for empty inputs", async () => {
const cases: Array<Parameters<typeof adapter.makeConfig>> = [
[{}],
[{}, { env: { TEST: "test" } }],
];
// merges everything returned from `app` with the config
expect(
omitKeys(
await adapter.makeConfig(
{ app: (a) => ({ config: { server: { cors: { origin: a.env.TEST } } } }) },
{ env: { TEST: "test" } },
),
["connection"],
),
).toEqual({
config: { server: { cors: { origin: "test" } } },
});
});
for (const args of cases) {
const cfg = await adapter.makeConfig(...(args as any));
expect(stripConnection(cfg)).toEqual({});
}
});
it("allows all properties in app function", async () => {
const called = mock(() => null);
const config = await adapter.makeConfig(
{
app: (env) => ({
connection: { url: "test" },
config: { server: { cors: { origin: "test" } } },
options: {
mode: "db",
},
onBuilt: () => {
called();
expect(env).toEqual({ foo: "bar" });
},
}),
},
{ foo: "bar" },
it("merges app output into config", async () => {
const cfg = await adapter.makeConfig(
{ app: (a) => ({ config: { server: { cors: { origin: a.env.TEST } } } }) },
{ env: { TEST: "test" } },
);
expect(config.connection).toEqual({ url: "test" });
expect(config.config).toEqual({ server: { cors: { origin: "test" } } });
expect(config.options).toEqual({ mode: "db" });
await config.onBuilt?.(null as any);
expect(called).toHaveBeenCalled();
});
adapterTestSuite(bunTestRunner, {
expect(stripConnection(cfg)).toEqual({
config: { server: { cors: { origin: "test" } } },
});
});
it("allows all properties in app() result", async () => {
const called = mock(() => null);
const cfg = await adapter.makeConfig(
{
app: (env) => ({
connection: { url: "test" },
config: { server: { cors: { origin: "test" } } },
options: { mode: "db" as const },
onBuilt: () => {
called();
expect(env).toEqual({ foo: "bar" });
},
}),
},
{ foo: "bar" },
);
expect(cfg.connection).toEqual({ url: "test" });
expect(cfg.config).toEqual({ server: { cors: { origin: "test" } } });
expect(cfg.options).toEqual({ mode: "db" });
await cfg.onBuilt?.({} as any);
expect(called).toHaveBeenCalledTimes(1);
});
});
describe("adapter test suites", () => {
adapterTestSuite(bunTestRunner, {
makeApp: adapter.createFrameworkApp,
label: "framework app",
});
});
adapterTestSuite(bunTestRunner, {
adapterTestSuite(bunTestRunner, {
makeApp: adapter.createRuntimeApp,
label: "runtime app",
});
});
});
});
+30 -4
View File
@@ -1,7 +1,7 @@
/// <reference types="@types/bun" />
import { describe, expect, it } from "bun:test";
import { describe, expect, it, mock } from "bun:test";
import { Hono } from "hono";
import { getFileFromContext, isFile, isReadableStream } from "core/utils";
import { getFileFromContext, isFile, isReadableStream, s, jsc } from "core/utils";
import { MediaApi } from "media/api/MediaApi";
import { assetsPath, assetsTmpPath } from "../helper";
@@ -98,7 +98,7 @@ describe("MediaApi", () => {
expect(isReadableStream(res.body)).toBe(true);
expect(isReadableStream(res.res.body)).toBe(true);
const blob = await res.res.blob();
const blob = (await res.res.blob()) as File;
expect(isFile(blob)).toBe(true);
expect(blob.size).toBeGreaterThan(0);
expect(blob.type).toBe("image/png");
@@ -113,7 +113,7 @@ describe("MediaApi", () => {
const res = await api.getFileStream(name);
expect(isReadableStream(res)).toBe(true);
const blob = await new Response(res).blob();
const blob = (await new Response(res).blob()) as File;
expect(isFile(blob)).toBe(true);
expect(blob.size).toBeGreaterThan(0);
expect(blob.type).toBe("image/png");
@@ -162,4 +162,30 @@ describe("MediaApi", () => {
await matches(api.upload(response.body!, { filename: "readable.png" }), "readable.png");
}
});
it("should add overwrite query for entity upload", async (c) => {
const call = mock(() => null);
const hono = new Hono().post(
"/api/media/entity/:entity/:id/:field",
jsc("query", s.object({ overwrite: s.boolean().optional() })),
async (c) => {
const { overwrite } = c.req.valid("query");
expect(overwrite).toBe(true);
call();
return c.json({ ok: true });
},
);
const api = new MediaApi(
{
upload_fetcher: hono.request,
},
hono.request,
);
const file = Bun.file(`${assetsPath}/image.png`);
const res = await api.uploadToEntity("posts", 1, "cover", file as any, {
overwrite: true,
});
expect(res.ok).toBe(true);
expect(call).toHaveBeenCalled();
});
});
@@ -8,6 +8,7 @@ import type { TAppMediaConfig } from "../../src/media/media-schema";
import { StorageLocalAdapter } from "adapter/node/storage/StorageLocalAdapter";
import { assetsPath, assetsTmpPath } from "../helper";
import { disableConsoleLog, enableConsoleLog } from "core/utils/test";
import * as proto from "data/prototype";
beforeAll(() => {
disableConsoleLog();
@@ -128,4 +129,87 @@ describe("MediaController", () => {
expect(destFile.exists()).resolves.toBe(true);
await destFile.delete();
});
test("entity upload with max_items and overwrite", async () => {
const app = createApp({
config: {
media: mergeObject(
{
enabled: true,
adapter: {
type: "local",
config: {
path: assetsTmpPath,
},
},
},
{},
),
data: {
entities: {
posts: proto
.entity("posts", {
title: proto.text(),
cover: proto.medium(),
})
.toJSON(),
},
},
},
});
await app.build();
// create a post first
const createRes = await app.server.request("/api/data/entity/posts", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ title: "Test Post" }),
});
expect(createRes.status).toBe(201);
const { data: post } = (await createRes.json()) as any;
const file = Bun.file(path);
const uploadedFiles: string[] = [];
// upload first file to entity (should succeed)
const res1 = await app.server.request(`/api/media/entity/posts/${post.id}/cover`, {
method: "POST",
body: file,
});
expect(res1.status).toBe(201);
const result1 = (await res1.json()) as any;
uploadedFiles.push(result1.name);
// upload second file without overwrite (should fail - max_items reached)
const res2 = await app.server.request(`/api/media/entity/posts/${post.id}/cover`, {
method: "POST",
body: file,
});
expect(res2.status).toBe(400);
const result2 = (await res2.json()) as any;
expect(result2.error).toContain("Max items");
// upload third file with overwrite=true (should succeed and delete old file)
const res3 = await app.server.request(
`/api/media/entity/posts/${post.id}/cover?overwrite=true`,
{
method: "POST",
body: file,
},
);
expect(res3.status).toBe(201);
const result3 = (await res3.json()) as any;
uploadedFiles.push(result3.name);
// verify old file was deleted from storage
const oldFile = Bun.file(assetsTmpPath + "/" + uploadedFiles[0]);
expect(await oldFile.exists()).toBe(false);
// verify new file exists
const newFile = Bun.file(assetsTmpPath + "/" + uploadedFiles[1]);
expect(await newFile.exists()).toBe(true);
// cleanup
await newFile.delete();
});
});
+13 -2
View File
@@ -2,7 +2,7 @@ import { $ } from "bun";
import * as tsup from "tsup";
import pkg from "./package.json" with { type: "json" };
import c from "picocolors";
import { watch as fsWatch } from "node:fs";
import { watch as fsWatch, readdirSync, rmSync } from "node:fs";
import { join } from "node:path";
const args = process.argv.slice(2);
@@ -27,7 +27,18 @@ const define = {
if (clean) {
console.info("Cleaning dist (w/o static)");
await $`find dist -mindepth 1 ! -path "dist/static/*" ! -path "dist/static" -exec rm -rf {} +`;
// Cross-platform clean: remove all files/folders in dist except static
const distPath = join(import.meta.dir, "dist");
try {
const entries = readdirSync(distPath);
for (const entry of entries) {
if (entry === "static") continue;
const entryPath = join(distPath, entry);
rmSync(entryPath, { recursive: true, force: true });
}
} catch (e) {
// dist may not exist yet, ignore
}
}
let types_running = false;
+3 -2
View File
@@ -46,7 +46,7 @@
"test:e2e:report": "VITE_DB_URL=:memory: playwright show-report",
"docs:build-assets": "bun internal/docs.build-assets.ts"
},
"license": "FSL-1.1-MIT",
"license": "Apache-2.0",
"dependencies": {
"@cfworker/json-schema": "^4.1.1",
"@codemirror/lang-html": "^6.4.11",
@@ -63,7 +63,7 @@
"dayjs": "^1.11.19",
"fast-xml-parser": "^5.3.1",
"hono": "4.10.4",
"json-schema-library": "10.0.0-rc7",
"json-schema-library": "10.0.0-rc7",
"json-schema-to-ts": "^3.1.1",
"jsonv-ts": "^0.10.1",
"kysely": "0.28.8",
@@ -108,6 +108,7 @@
"clsx": "^2.1.1",
"commander": "^14.0.2",
"dotenv": "^17.2.3",
"errore": "^0.14.0",
"jotai": "^2.12.2",
"jsdom": "^26.1.0",
"kysely-generic-sqlite": "^1.2.1",
@@ -9,7 +9,8 @@ afterAll(enableConsoleLog);
describe("sveltekit adapter", () => {
adapterTestSuite(bunTestRunner, {
makeApp: (c, a) => sveltekit.getApp(c, a ?? ({} as any)),
makeHandler: (c, a) => (request: Request) => sveltekit.serve(c, a ?? ({} as any))({ request }),
makeApp: (c, a) => sveltekit.getApp(c as any, a ?? ({} as any)),
makeHandler: (c, a) => (request: Request) =>
sveltekit.serve(c as any, a ?? ({} as any))({ request }),
});
});
+149 -6
View File
@@ -4,7 +4,9 @@ import type { CliBkndConfig, CliCommand } from "cli/types";
import { Option } from "commander";
import { config, type App, type CreateAppConfig, type MaybePromise, registries } from "bknd";
import dotenv from "dotenv";
import * as errore from "errore";
import c from "picocolors";
import { execFileSync } from "node:child_process";
import path from "node:path";
import {
PLATFORMS,
@@ -18,6 +20,26 @@ import { createRuntimeApp, makeConfig } from "bknd/adapter";
import { colorizeConsole, isBun } from "bknd/utils";
import { withConfigOptions, type WithConfigOptions } from "cli/utils/options";
class ConfigLoadError extends errore.createTaggedError({
name: "ConfigLoadError",
message: "Failed to load config: $reason",
}) {}
class NeedsBunError extends errore.createTaggedError({
name: "NeedsBunError",
message: "Config requires Bun runtime",
}) {}
class NeedsTypeStrippingError extends errore.createTaggedError({
name: "NeedsTypeStrippingError",
message: "Node $version needs --experimental-strip-types for .ts config",
}) {}
class ReexecFailedError extends errore.createTaggedError({
name: "ReexecFailedError",
message: "Re-exec failed: $reason",
}) {}
const env_files = [".env", ".dev.vars"];
dotenv.config({
path: env_files.map((file) => path.resolve(process.cwd(), file)),
@@ -55,6 +77,106 @@ if (!registries.media.has(local)) {
registries.media.register(local, StorageLocalAdapter);
}
function needsTypeStripping(configFilePath: string): boolean {
if (!/\.[mc]?ts$/.test(configFilePath)) return false;
const [major, minor] = process.versions.node.split(".").map(Number);
// Node v22.06 introduced experimental TypeScript support via strip types.
return major === 22 && minor! < 18 && minor! > 5;
}
function reexecWithTypeStripping(): ReexecFailedError | never {
if (process.env.__BKND_REEXEC) {
return new ReexecFailedError({
reason: "TS config still failed after re-exec with --experimental-strip-types",
});
}
const cliPath = path.resolve(process.argv[1]!);
const args = ["--experimental-strip-types", cliPath, ...process.argv.slice(2)];
console.info(
c.yellow("Node <22.18 detected, re-executing with --experimental-strip-types"),
);
const result = errore.try({
try: () =>
execFileSync(process.execPath, args, {
stdio: "inherit",
env: { ...process.env, __BKND_REEXEC: "1" },
}),
catch: (e) =>
new ReexecFailedError({
reason: "Failed to re-exec with --experimental-strip-types",
cause: e,
}),
});
if (result instanceof Error) return result;
process.exit(0);
}
async function loadConfigFile(
configFilePath: string,
): Promise<ConfigLoadError | NeedsBunError | NeedsTypeStrippingError | CliBkndConfig> {
if (needsTypeStripping(configFilePath) && !process.execArgv.includes("--experimental-strip-types")) {
return new NeedsTypeStrippingError({
version: process.versions.node,
});
}
const result = await errore.tryAsync({
try: () => import(configFilePath).then((m) => m.default as CliBkndConfig),
catch: (e) => {
const needsBun =
(e instanceof ReferenceError && /\bBun\b.*not defined/.test(e.message)) ||
(e instanceof Error && "code" in e && e.code === "ERR_UNSUPPORTED_ESM_URL_SCHEME" && /bun:/.test(e.message));
if (needsBun) return new NeedsBunError();
return new ConfigLoadError({
reason: e instanceof Error ? e.message : String(e),
cause: e,
});
},
});
return result;
}
function reexecUnderBun(): ReexecFailedError | never {
if (process.env.__BKND_REEXEC) {
return new ReexecFailedError({
reason: "Config requires Bun but still failed under Bun runtime",
});
}
const bunPath = process.env.BUN_INSTALL
? path.join(process.env.BUN_INSTALL, "bin", "bun")
: "bun";
const cliPath = path.resolve(process.argv[1]!);
const args = [cliPath, ...process.argv.slice(2)];
console.info(
c.yellow("Config requires Bun runtime, re-executing:"),
c.cyan(`bun ${args.join(" ")}`),
);
const result = errore.try({
try: () =>
execFileSync(bunPath, args, {
stdio: "inherit",
env: { ...process.env, __BKND_REEXEC: "1" },
}),
catch: (e) =>
new ReexecFailedError({
reason: "Could not re-exec under Bun. Install Bun (https://bun.sh) or use bknd/adapter/node in your config.",
cause: e,
}),
});
if (result instanceof Error) return result;
process.exit(0);
}
type MakeAppConfig = {
connection?: CreateAppConfig["connection"];
server?: { platform?: Platform };
@@ -99,12 +221,33 @@ export async function makeAppFromEnv(options: Partial<RunOptions> = {}) {
// check configuration file to be present
} else if (configFilePath) {
console.info("Using config from", c.cyan(configFilePath));
try {
const config = (await import(configFilePath).then((m) => m.default)) as CliBkndConfig;
app = await makeConfigApp(config, options.server);
} catch (e) {
console.error("Failed to load config:", e);
process.exit(1);
const configResult = await loadConfigFile(configFilePath);
if (configResult instanceof Error) {
errore.matchError(configResult, {
NeedsTypeStrippingError: () => {
const reexecResult = reexecWithTypeStripping();
// only reached on failure — success calls process.exit(0)
console.error(c.red(reexecResult.message));
process.exit(1);
},
NeedsBunError: () => {
const reexecResult = reexecUnderBun();
// only reached on failure — success calls process.exit(0)
console.error(c.red(reexecResult.message));
process.exit(1);
},
ConfigLoadError: (e) => {
console.error(c.red("Failed to load config:"), e.reason);
process.exit(1);
},
Error: (e) => {
console.error(c.red("Failed to load config:"), e.message);
process.exit(1);
},
});
} else {
app = await makeConfigApp(configResult, options.server);
}
// try to use an in-memory connection
+17 -4
View File
@@ -71,11 +71,12 @@ export class MediaApi extends ModuleApi<MediaApiOptions> {
}
protected uploadFile<T extends FileUploadedEventData>(
body: File | Blob | ReadableStream | Buffer<ArrayBufferLike>,
body: BodyInit,
opts?: {
filename?: string;
path?: TInput;
_init?: Omit<RequestInit, "body">;
query?: Record<string, any>;
},
): FetchPromise<ResponseObject<T>> {
const headers = {
@@ -102,14 +103,22 @@ export class MediaApi extends ModuleApi<MediaApiOptions> {
headers,
};
if (opts?.path) {
return this.post(opts.path, body, init);
return this.request<T>(opts.path, opts?.query, {
...init,
body,
method: "POST",
});
}
if (!name || name.length === 0) {
throw new Error("Invalid filename");
}
return this.post<T>(opts?.path ?? ["upload", name], body, init);
return this.request<T>(opts?.path ?? ["upload", name], opts?.query, {
...init,
body,
method: "POST",
});
}
async upload<T extends FileUploadedEventData>(
@@ -119,6 +128,7 @@ export class MediaApi extends ModuleApi<MediaApiOptions> {
_init?: Omit<RequestInit, "body">;
path?: TInput;
fetcher?: ApiFetcher;
query?: Record<string, any>;
} = {},
) {
if (item instanceof Request || typeof item === "string") {
@@ -144,7 +154,7 @@ export class MediaApi extends ModuleApi<MediaApiOptions> {
});
}
return this.uploadFile<T>(item, opts);
return this.uploadFile<T>(item as BodyInit, opts);
}
async uploadToEntity(
@@ -155,11 +165,14 @@ export class MediaApi extends ModuleApi<MediaApiOptions> {
opts?: {
_init?: Omit<RequestInit, "body">;
fetcher?: typeof fetch;
overwrite?: boolean;
},
): Promise<ResponseObject<FileUploadedEventData & { result: DB["media"] }>> {
const query = opts?.overwrite !== undefined ? { overwrite: opts.overwrite } : undefined;
return this.upload(item, {
...opts,
path: ["entity", entity, id, field],
query,
});
}
+5 -3
View File
@@ -16,9 +16,11 @@ import ToolsRoutes from "./tools";
// @ts-ignore
let TestRoutes: any;
if (import.meta.env.DEV) {
TestRoutes = lazy(() => import("./test"));
}
try {
if (import.meta.env.DEV) {
TestRoutes = lazy(() => import("./test"));
}
} catch {}
export function Routes({
BkndWrapper,
+7 -8
View File
@@ -16,7 +16,7 @@
},
"app": {
"name": "bknd",
"version": "0.20.0-rc.1",
"version": "0.20.0",
"bin": "./dist/cli/index.js",
"dependencies": {
"@cfworker/json-schema": "^4.1.1",
@@ -79,6 +79,7 @@
"clsx": "^2.1.1",
"commander": "^14.0.2",
"dotenv": "^17.2.3",
"errore": "^0.14.0",
"jotai": "^2.12.2",
"jsdom": "^26.1.0",
"kysely-generic-sqlite": "^1.2.1",
@@ -1936,6 +1937,8 @@
"error-stack-parser-es": ["error-stack-parser-es@1.0.5", "", {}, "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA=="],
"errore": ["errore@0.14.0", "", { "bin": { "errore": "dist/cli.js" } }, "sha512-2RI7BGdxWlJe6yJ3DK0pIvLNzXEE4M41VmF2HrH7C2HldTjYiORQNJ+ufsvAvOUGW/4rRHPFOpooP3ebIRIEXw=="],
"es-abstract": ["es-abstract@1.23.9", "", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.3", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.0", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-regex": "^1.2.1", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.0", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.3", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.3", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.18" } }, "sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA=="],
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
@@ -2462,7 +2465,7 @@
"jest-worker": ["jest-worker@24.9.0", "", { "dependencies": { "merge-stream": "^2.0.0", "supports-color": "^6.1.0" } }, "sha512-51PE4haMSXcHohnSMdM42anbvZANYTqMrr52tVKPqqsPJMzoP6FYYDVqahX/HrAoKEKz3uUPzSvKs9A3qR4iVw=="],
"jiti": ["jiti@1.21.7", "", { "bin": { "jiti": "bin/jiti.js" } }, "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A=="],
"jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
"jotai": ["jotai@2.15.1", "", { "peerDependencies": { "@babel/core": ">=7.0.0", "@babel/template": ">=7.0.0", "@types/react": ">=17.0.0", "react": ">=17.0.0" }, "optionalPeers": ["@babel/core", "@babel/template", "@types/react", "react"] }, "sha512-yHT1HAZ3ba2Q8wgaUQ+xfBzEtcS8ie687I8XVCBinfg4bNniyqLIN+utPXWKQE93LMF5fPbQSVRZqgpcN5yd6Q=="],
@@ -3846,7 +3849,7 @@
"@babel/traverse/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="],
"@bknd/plasmic/@types/bun": ["@types/bun@1.3.3", "", { "dependencies": { "bun-types": "1.3.3" } }, "sha512-ogrKbJ2X5N0kWLLFKeytG0eHDleBYtngtlbu9cyBKFtNL3cnpDZkNdQj8flVf6WTZUX5ulI9AY1oa7ljhSrp+g=="],
"@bknd/plasmic/@types/bun": ["@types/bun@1.3.10", "", { "dependencies": { "bun-types": "1.3.10" } }, "sha512-0+rlrUrOrTSskibryHbvQkDOWRJwJZqZlxrUs1u4oOoTln8+WIXBPmAuCF35SWB2z4Zl3E84Nl/D0P7803nigQ=="],
"@bundled-es-modules/tough-cookie/tough-cookie": ["tough-cookie@4.1.4", "", { "dependencies": { "psl": "^1.1.33", "punycode": "^2.1.1", "universalify": "^0.2.0", "url-parse": "^1.5.3" } }, "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag=="],
@@ -4046,8 +4049,6 @@
"@swc/helpers/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@tailwindcss/node/jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
"@tailwindcss/node/lightningcss": ["lightningcss@1.30.2", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.30.2", "lightningcss-darwin-arm64": "1.30.2", "lightningcss-darwin-x64": "1.30.2", "lightningcss-freebsd-x64": "1.30.2", "lightningcss-linux-arm-gnueabihf": "1.30.2", "lightningcss-linux-arm64-gnu": "1.30.2", "lightningcss-linux-arm64-musl": "1.30.2", "lightningcss-linux-x64-gnu": "1.30.2", "lightningcss-linux-x64-musl": "1.30.2", "lightningcss-win32-arm64-msvc": "1.30.2", "lightningcss-win32-x64-msvc": "1.30.2" } }, "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.6.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-zq/ay+9fNIJJtJiZxdTnXS20PllcYMX3OE23ESc4HK/bdYu3cOWYVhsOhVnXALfU/uqJIxn5NBPd9z4v+SfoSg=="],
@@ -4750,7 +4751,7 @@
"@babel/preset-env/babel-plugin-polyfill-regenerator/@babel/helper-define-polyfill-provider": ["@babel/helper-define-polyfill-provider@0.6.3", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.22.6", "@babel/helper-plugin-utils": "^7.22.5", "debug": "^4.1.1", "lodash.debounce": "^4.0.8", "resolve": "^1.14.2" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-HK7Bi+Hj6H+VTHA3ZvBis7V/6hu9QuTrnMXNybfUf2iiuU/N97I8VjB+KbhFF8Rld/Lx5MzoCwPCpPjfK+n8Cg=="],
"@bknd/plasmic/@types/bun/bun-types": ["bun-types@1.3.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-z3Xwlg7j2l9JY27x5Qn3Wlyos8YAp0kKRlrePAOjgjMGS5IG6E7Jnlx736vH9UVI4wUICwwhC9anYL++XeOgTQ=="],
"@bknd/plasmic/@types/bun/bun-types": ["bun-types@1.3.10", "", { "dependencies": { "@types/node": "*" } }, "sha512-tcpfCCl6XWo6nCVnpcVrxQ+9AYN1iqMIzgrSKYMB/fjLtV2eyAVEg7AxQJuCq/26R6HpKWykQXuSOq/21RYcbg=="],
"@bundled-es-modules/tough-cookie/tough-cookie/universalify": ["universalify@0.2.0", "", {}, "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg=="],
@@ -5236,8 +5237,6 @@
"@babel/preset-env/babel-plugin-polyfill-regenerator/@babel/helper-define-polyfill-provider/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="],
"@bknd/plasmic/@types/bun/bun-types/@types/node": ["@types/node@24.10.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A=="],
"@cloudflare/vitest-pool-workers/miniflare/workerd/@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20251011.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-0DirVP+Z82RtZLlK2B+VhLOkk+ShBqDYO/jhcRw4oVlp0TOvk3cOVZChrt3+y3NV8Y/PYgTEywzLKFSziK4wCg=="],
"@cloudflare/vitest-pool-workers/miniflare/workerd/@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20251011.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-1WuFBGwZd15p4xssGN/48OE2oqokIuc51YvHvyNivyV8IYnAs3G9bJNGWth1X7iMDPe4g44pZrKhRnISS2+5dA=="],
@@ -7,7 +7,7 @@ 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)
- fully manage your backend visually when run in [`db` mode](/usage/setup/#ui-only-mode)
- manage your database contents
- manage your media contents
@@ -121,7 +121,7 @@ If the connection object is omitted, the app will try to use an in-memory databa
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:
to the latest version upon boot ([`db` mode](/usage/setup#ui-only-mode) only). The default configuration looks like this:
```json
{
@@ -210,6 +210,10 @@ to the latest version upon boot ([`db` mode](/usage/introduction#ui-only-mode) o
}
```
<Callout type="warn" title="Guard lockout warning">
Setting `auth.guard.enabled` to `true` without first creating a user with an admin role will lock you out of the admin portal. See [Securing Your Admin Portal](/modules/auth#securing-your-admin-portal) for the required steps before enabling the Guard.
</Callout>
You can use the [CLI](/usage/cli/#getting-the-configuration-config) to get the default configuration:
```sh
@@ -170,7 +170,7 @@ export const prerender = false;
theme: "dark",
logo_return_path: "/../"
}}
client:only
client:only="react"
/>
</body>
</html>
@@ -214,7 +214,7 @@ Now you can use the CLI with your Cloudflare resources.
Instead, it's recommended to split this configuration into separate files, e.g. `bknd.config.ts` and `config.ts`:
```typescript title="config.ts"
import type { CloudflareBkndConfig } from "bknd/adapter/cloudflare";
import { d1, type CloudflareBkndConfig } from "bknd/adapter/cloudflare";
export default {
app: (env) => ({
@@ -236,4 +236,4 @@ As an additional safe guard, you have to set a `PROXY` environment variable to `
```bash
PROXY=1 npx bknd types
```
```
+1 -1
View File
@@ -9,7 +9,7 @@
"start",
"motivation",
"---Usage---",
"./usage/introduction",
"./usage/setup",
"./usage/database",
"./usage/cli",
"./usage/sdk",
@@ -27,3 +27,69 @@ Authentication is essential for securing applications, and **bknd** provides a s
- Compatible with any specification-compliant provider.
With a focus on flexibility and ease of integration, bknd's authentication system offers the essentials for managing secure user access in your applications.
---
## Server Module: `module.auth`
### `auth.changePassword([userId], [password])`
To change a user's password, use the `changePassword` method:
```ts
await app.module.auth.changePassword(user.id, password);
```
This method updates the password for the specified user.
- `userId`: The ID of the user whose password should be changed.
- `password`: The new password value.
This method throws an error if:
- The user with the given `userId` is not found.
- The user's authentication strategy is not `"password"` (e.g. the user registered via an OAuth provider).
## Securing Your Admin Portal
<Callout type="warn" title="Do not enable the Guard without an admin user">
Enabling the Guard without first creating a user with an admin role **will lock you out of the admin portal entirely**. There is no login screen that can save you — you'll need to manually edit the database to recover. Follow the checklist below before enabling the Guard.
</Callout>
The **Guard** protects your admin portal and API endpoints by requiring authentication and proper permissions. Before enabling it, you must set up at least one user with full admin access.
### Checklist Before Enabling Guard
Complete these steps **in order** before turning on the Guard:
1. **Create an admin role** with `implicit_allow: true`
- This grants full access to all permissions
- Go to Auth → Roles → Create a new role
- Enable the "Implicit Allow" toggle
2. **Create a user**
- Go to Auth → Users → Create a new user
- Set up their email and password
3. **Attach the admin role to the user**
- Edit the user you just created
- Assign the admin role to them
4. **Verify you can sign in**
- Open an incognito/private browser window
- Navigate to your app and sign in with the admin user
- Confirm you have access
5. **Now enable the Guard**
- Go to Auth → Settings
- Enable the Guard
### Recovery: If You're Locked Out
If you enabled the Guard without setting up an admin user, you'll need to access your database directly:
1. Connect to your database using a database client or CLI tool
2. Find the `__bknd` table
3. Locate the row where `type = 'config'`
4. In the `json` column, set `auth.guard.enabled` to `false`
5. Restart your bknd instance
6. Complete the checklist above, then re-enable the Guard
+68 -43
View File
@@ -6,38 +6,36 @@ tags: ["documentation"]
import { Icon } from "@iconify/react";
import { examples } from "@/app/_components/StackBlitz";
import { SquareMousePointer, Code, Blend } from 'lucide-react';
import { SquareMousePointer, Code, Blend, Rocket } from 'lucide-react';
## bknd is a lightweight batteries-included backend that embeds into your frontend app
Glad you're here! **bknd** is a lightweight, infrastructure agnostic and feature-rich backend that runs in any JavaScript environment.
<Callout type="warning" title="We are in beta">
We're making great progress towards v1, but don't recommend production use yet.
Follow along for updates on [GitHub Releases](https://github.com/bknd-io/bknd/releases) and in our [Discord community](https://discord.gg/952SFk8Tb8)
</Callout>
- Instant backend with full REST API
- Built on Web Standards for maximum compatibility
- Multiple ready-made [integrations](/integration/introduction) (standalone, runtime, framework)
- Official [API SDK](/usage/sdk) and [React SDK](/usage/react) with type-safety
- [React elements](/usage/elements) for auto-configured authentication and media components
- Built-in [MCP server](/usage/mcp/overview) for controlling your backend
- Multiple run [modes](/usage/introduction#modes) (ui-only, code-only, hybrid)
bknd includes full REST APIs, an admin dashboard, auth, media uploads, a [type-safe SDK](/usage/sdk), [React hooks](/usage/react), and plugins to extend it. Host it with your SSR app or as a standalone service. Built on Web Standards, it runs anywhere JavaScript runs.
## Preview
Bring your [favorite frontend](./#start-with-a-frameworkruntime) and [favorite SQL database](./#use-your-favorite-sql-database), and we'll bring the ~~backend~~ bknd.
Here is a preview of **bknd** in StackBlitz:
<Card className="p-0 pb-1">
<StackBlitz path="github/bknd-io/bknd-demo" initialPath="/" />
<Accordions className="m-1">
<Accordion title="What's going on?">
The example shown is starting a [node server](/integration/node) using an [in-memory database](/usage/database#sqlite-in-memory). To ensure there are a few entities defined, it is using an [initial structure](/usage/database#initial-structure) using the prototype methods. Furthermore it uses the [seed option](/usage/database#seeding-the-database) to seed some data in the structure created.
To ensure there are users defined on first boot, it hooks into the `App.Events.AppFirstBoot` event to create them (documentation pending).
</Accordion>
</Accordions>
</Card>
<Cards>
<Card
href="/motivation"
title="Learn about why we built bknd"
icon={<Rocket />}
>
Why another backend system?
</Card>
</Cards>
## Quickstart
Enter the following command to spin up an instance:
<Callout type="info" title="This demo bknd instance is for playing and learning">
Don't worry about messing anything up in this stage since you're learning the ropes of bknd. If you want to start over, please delete the generated `data.db` database file and follow this tutorial again.
</Callout>
Spin up a bknd instance via the [bknd CLI](/usage/cli):
<Tabs groupId='package-manager' persist items={[ 'npm','bun' ]}>
@@ -51,11 +49,54 @@ bunx bknd run
</Tabs>
To learn more about the CLI, check out the [Using the CLI](/usage/cli) guide.
This creates a local `data.db` SQLite database and starts the bknd web server at http://localhost:1337.
By default, the admin dashboard is open and not guarded. This is intentional — bknd uses an opt-in philosophy to allow quick prototyping. Let's enable authentication and guard the dashboard to secure it.
1. Visit http://localhost:1337/auth/settings. Toggle "Authentication Enabled" to enable auth. Select "Update" to save.
2. Visit http://localhost:1337/data/entity/users. Create a user by selecting "New User" and entering an email and password.
3. Visit http://localhost:1337/auth/roles. Create an admin role by selecting "Create new". Name it "Admin" and select "Create". Then, scroll to the bottom of the page and enable "Implicit allow missing permissions?". Select "Update" to save.
4. Visit http://localhost:1337/data/entity/users. Select on your newly created user, select the "Admin" role, and select "Update".
5. It's time to guard your admin dashboard. Visit http://localhost:1337/settings/auth. Select "Edit". Scroll to "Guard" and enable it. Then, select "Save". _(This should log you out!)_
6. Now, log in to your secured admin dashboard at http://localhost:1337/auth/login.
You did it! You've started the bknd server, created an admin user, and protected your app by enabling the guard.
## Modes
What you just experienced is **UI-only mode** — bknd's data and configuration is managed entirely via the admin dashboard. But that's not the only way to use bknd:
<Cards className="grid-cols-3">
<Card title="UI-only" href="/usage/setup#ui-only-mode" icon={<SquareMousePointer className="text-fd-primary !size-6" />}>
Configure your backend and manage your data visually with the built-in Admin UI.
</Card>
<Card title="Code-only" href="/usage/setup#code-only-mode" icon={<Code className="text-fd-primary !size-6" />}>
Configure your backend programmatically with a Drizzle-like API, manage your data with the Admin UI.
</Card>
<Card title="Hybrid" href="/usage/setup#hybrid-mode" icon={<Blend className="text-fd-primary !size-6" />}>
Configure your backend visually while in development, use a read-only configuration in production.
</Card>
</Cards>
Learn more about each mode and the underlying configuration in [Setup & Modes](/usage/setup).
## Try bknd in the browser
<Card className="p-0 pb-1">
<StackBlitz path="github/bknd-io/bknd-demo" initialPath="/" />
<Accordions className="m-1">
<Accordion title="What's going on?">
The example shown is starting a [node server](/integration/node) using an [in-memory database](/usage/database#sqlite-in-memory). To ensure there are a few entities defined, it is using an [initial structure](/usage/database#initial-structure) using the prototype methods. Furthermore it uses the [seed option](/usage/database#seeding-the-database) to seed some data in the structure created.
To ensure there are users defined on first boot, it hooks into the `App.Events.AppFirstBoot` event to create them (documentation pending).
</Accordion>
</Accordions>
</Card>
## Start with a Framework/Runtime
Start by using the integration guide for these popular frameworks/runtimes. There will be more in the future, so stay tuned!
Pick your framework or runtime to get started.
<Cards>
<Card icon={<Icon icon="tabler:brand-nextjs" className="text-fd-primary !size-6" />} title="NextJS" href="/integration/nextjs" />
@@ -116,7 +157,7 @@ Start by using the integration guide for these popular frameworks/runtimes. Ther
/>
<Card
icon={<Icon icon="simple-icons:vitest" className="text-fd-primary !size-6" />}
icon={<Icon icon="simple-icons:vite" className="text-fd-primary !size-6" />}
title="Vite"
href="/integration/vite"
/>
@@ -173,19 +214,3 @@ The following databases are currently supported. Request a new integration if yo
Create a new issue to request a new database integration.
</Card>
</Cards>
## Choose a mode
**bknd** supports multiple modes to suit your needs.
<Cards className="grid-cols-3">
<Card title="UI-only" href="/usage/introduction#ui-only-mode" icon={<SquareMousePointer className="text-fd-primary !size-6" />}>
Configure your backend and manage your data visually with the built-in Admin UI.
</Card>
<Card title="Code-only" href="/usage/introduction#code-only-mode" icon={<Code className="text-fd-primary !size-6" />}>
Configure your backend programmatically with a Drizzle-like API, manage your data with the Admin UI.
</Card>
<Card title="Hybrid" href="/usage/introduction#hybrid-mode" icon={<Blend className="text-fd-primary !size-6" />}>
Configure your backend visually while in development, use a read-only configuration in production.
</Card>
</Cards>
@@ -95,27 +95,12 @@ export default {
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:
TypeScript configuration files (`.ts`) are fully supported and work out of the box with `npx bknd run` — no additional flags or tools required.
```
[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):
If your config imports from a runtime-specific adapter (e.g. `bknd/adapter/bun`), you need to run the CLI with that runtime:
```sh
node --experimental-strip-types node_modules/.bin/bknd run
```
Or with `tsx`:
```sh
npx tsx node_modules/.bin/bknd run
bun node_modules/.bin/bknd run
```
### Turso/LibSQL database
@@ -263,7 +248,7 @@ To automatically sync your secrets to a file, you may also use the [`syncSecrets
## Syncing the database (`sync`)
Sync your database can be useful when running in [`code`](/usage/introduction/#code-only-mode) mode. When you're ready to deploy, you can point to the production configuration and sync the database. Schema mutations are only applied when running with the `--force` option.
Sync your database can be useful when running in [`code`](/usage/setup/#code-only-mode) mode. When you're ready to deploy, you can point to the production configuration and sync the database. Schema mutations are only applied when running with the `--force` option.
```bash
$ npx bknd sync --help
@@ -310,7 +310,7 @@ const app = createApp({ connection });
## Data Structure
To provide a database structure, you can pass `config` to the creation of an app. In [`db` mode](/usage/introduction#ui-only-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.
To provide a database structure, you can pass `config` to the creation of an app. In [`db` mode](/usage/setup#ui-only-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:
@@ -506,9 +506,9 @@ const app = createApp({
});
```
Note that in [`db` mode](/usage/introduction#ui-only-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.
Note that in [`db` mode](/usage/setup#ui-only-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](/usage/introduction#code-only-mode), the seed function will not be automatically executed. You can manually execute it by running the following command:
In [`code` mode](/usage/setup#code-only-mode), the seed function will not be automatically executed. You can manually execute it by running the following command:
```bash
npx bknd sync --seed --force
@@ -1,264 +0,0 @@
---
title: "Introduction"
description: "Setting up bknd"
icon: Pin
tags: ["documentation"]
---
import { TypeTable } from "fumadocs-ui/components/type-table";
import { SquareMousePointer, Code, Blend } from 'lucide-react';
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 BkndConfig } from "bknd";
// create the app
const config = {
/* ... */
} satisfies BkndConfig;
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. In case an explicit `fetch` export is needed, you can use the `app.fetch` property.
```typescript
const app = /* ... */;
export default {
fetch: app.fetch,
}
```
Check the integration details for your specific runtime or framework in the [integration](/integration/introduction) section.
## Modes
Main project goal is to provide a backend that can be configured visually with the built-in Admin UI. However, you may instead want to configure your backend programmatically, and define your data structure with a Drizzle-like API:
<Cards className="grid-cols-1 sm:grid-cols-2 md:grid-cols-3">
<Card title="UI-only (default)" href="/usage/introduction#ui-only-mode" icon={<SquareMousePointer className="text-fd-primary !size-6" />}>
This is the default mode, it allows visual configuration and saves the configuration to the database. Expects you to deploy your backend separately from your frontend.
</Card>
<Card title="Code-only" href="/usage/introduction#code-only-mode" icon={<Code className="text-fd-primary !size-6" />}>
This mode allows you to configure your backend programmatically, and define your data structure with a Drizzle-like API. Visual configuration controls are disabled.
</Card>
<Card title={"Hybrid"} href="/usage/introduction#hybrid-mode" icon={<Blend className="text-fd-primary !size-6" />}>
This mode allows you to configure your backend visually while in development, and uses the produced configuration in a code-only mode for maximum performance.
</Card>
</Cards>
In the following sections, we'll cover the different modes in more detail. The configuration properties involved are the following:
```typescript title="bknd.config.ts"
import type { BkndConfig } from "bknd";
export default {
config: { /* ... */ }
options: {
mode: "db", // or "code"
manager: {
secrets: { /* ... */ },
storeSecrets: true,
},
}
} satisfies BkndConfig;
```
<TypeTable type={{
config: {
description: "The initial configuration when `mode` is `\"db\"`, and as the produced configuration when `mode` is `\"code\"`.",
type: "object",
properties: {
/* ... */
}
},
["options.mode"]: {
description: "The options for the app.",
type: '"db" | "code"',
default: '"db"'
},
["options.manager.secrets"]: {
description: "The app secrets to be provided when using `\"db\"` mode. This is required since secrets are extracted and stored separately to the database.",
type: "object",
properties: {
/* ... */
}
},
["options.manager.storeSecrets"]: {
description: "Whether to store secrets in the database when using `\"db\"` mode.",
type: "boolean",
default: "true"
}
}} />
### UI-only mode
This mode is the default mode. It allows you to configure your backend visually with the built-in Admin UI. It expects that you deploy your backend separately from your frontend, and make changes there. No configuration is needed, however, if you want to provide an initial configuration, you can do so by passing a `config` object.
```typescript
import type { BkndConfig } from "bknd";
export default {
// this will only be applied if the database is empty
config: { /* ... */ },
} satisfies BkndConfig;
```
<Callout type="info">
Note that when using the default UI-mode, the initial configuration using the `config` property will only be applied if the database is empty.
</Callout>
### Code-only mode
This mode allows you to configure your backend programmatically, and define your data structure with a Drizzle-like API. Visual configuration controls are disabled.
```typescript title="bknd.config.ts"
import { type BkndConfig, em, entity, text, boolean } from "bknd";
import { secureRandomString } from "bknd/utils";
const schema = em({
todos: entity("todos", {
title: text(),
done: boolean(),
}),
});
export default {
// example configuration
config: {
data: schema.toJSON(),
auth: {
enabled: true,
jwt: {
secret: secureRandomString(64),
},
}
},
options: {
// this ensures that the provided configuration is always used
mode: "code",
},
} satisfies BkndConfig;
```
Unlike the UI-only mode, the configuration passed to `config` is always applied. In case you make data structure changes, you may need to sync the schema to the database manually, e.g. using the [sync command](/usage/cli#syncing-the-database-sync).
### Hybrid mode
This mode allows you to configure your backend visually while in development, and uses the produced configuration in a code-only mode for maximum performance. It gives you the best of both worlds.
While in development, we set the mode to `"db"` where the configuration is stored in the database. When it's time to deploy, we export the configuration, and set the mode to `"code"`. While in `"db"` mode, the `config` property interprets the value as an initial configuration to use when the database is empty.
```typescript title="bknd.config.ts"
import type { BkndConfig } from "bknd";
// import your produced configuration
import appConfig from "./appconfig.json" with { type: "json" };
export default {
config: appConfig,
options: {
mode: process.env.NODE_ENV === "development" ? "db" : "code",
manager: {
secrets: process.env
}
},
} satisfies BkndConfig;
```
To keep your config, secrets and types in sync, you can either use the CLI or the plugins.
| Type | Plugin | CLI Command |
|----------------|-----------------------------------------------------------------------|----------------------------|
| Configuration | [`syncConfig`](/extending/plugins/#syncconfig) | [`config`](/usage/cli/#getting-the-configuration-config) |
| Secrets | [`syncSecrets`](/extending/plugins/#syncsecrets) | [`secrets`](/usage/cli/#getting-the-secrets-secrets) |
| Types | [`syncTypes`](/extending/plugins/#synctypes) | [`types`](/usage/cli/#generating-types-types) |
## Mode helpers
To make the setup using your preferred mode easier, there are mode helpers for [`code`](/usage/introduction#code-only-mode) and [`hybrid`](/usage/introduction#hybrid-mode) modes.
* built-in syncing of config, types and secrets
* let bknd automatically sync the data schema in development
* automatically switch modes in hybrid (from db to code) in production
* automatically skip config validation in production to boost performance
To use it, you have to wrap your configuration in a mode helper, e.g. for `code` mode using the Bun adapter:
```typescript title="bknd.config.ts"
import { code, type CodeMode } from "bknd/modes";
import { type BunBkndConfig, writer } from "bknd/adapter/bun";
export default code<BunBkndConfig>({
// some normal bun bknd config
connection: { url: "file:data.db" },
// ...
// a writer is required, to sync the types
writer,
// (optional) mode specific config
isProduction: Bun.env.NODE_ENV === "production",
typesFilePath: "bknd-types.d.ts",
// (optional) e.g. have the schema synced if !isProduction
syncSchema: {
force: true,
drop: true,
}
});
```
Similarily, for `hybrid` mode:
```typescript title="bknd.config.ts"
import { hybrid, type HybridMode } from "bknd/modes";
import { type BunBkndConfig, writer, reader } from "bknd/adapter/bun";
export default hybrid<BunBkndConfig>({
// some normal bun bknd config
connection: { url: "file:data.db" },
// ...
// reader/writer are required, to sync the types and config
writer,
reader,
// supply secrets
secrets: await Bun.file(".env.local").json(),
// (optional) mode specific config
isProduction: Bun.env.NODE_ENV === "production",
typesFilePath: "bknd-types.d.ts",
configFilePath: "bknd-config.json",
// (optional) and have them automatically written if !isProduction
syncSecrets: {
outFile: ".env.local",
format: "env",
includeSecrets: true,
},
// (optional) also have the schema synced if !isProduction
syncSchema: {
force: true,
drop: true,
},
});
```
@@ -0,0 +1,180 @@
---
title: "Setup & Modes"
description: "Choose a mode and get bknd running with your app"
icon: Pin
tags: ["documentation"]
---
import { SquareMousePointer, Code, Blend } from 'lucide-react';
bknd supports three modes. Each mode determines how your backend is configured and where that configuration lives.
## Choose your mode
<Cards className="grid-cols-1 sm:grid-cols-2 md:grid-cols-3">
<Card title="UI-only (default)" href="#ui-only-mode" icon={<SquareMousePointer className="text-fd-primary !size-6" />}>
Configure visually via the Admin UI. Config is stored in the database.
</Card>
<Card title="Code-only" href="#code-only-mode" icon={<Code className="text-fd-primary !size-6" />}>
Define your schema in code with a Drizzle-like API. Config lives in your codebase.
</Card>
<Card title="Hybrid" href="#hybrid-mode" icon={<Blend className="text-fd-primary !size-6" />}>
Use the Admin UI in development, export to code for production.
</Card>
</Cards>
**Not sure which to pick?**
- **UI-only** is best for prototyping and small projects. No code needed — configure everything through the dashboard.
- **Code-only** is best for teams, CI/CD, and version-controlled schemas. You define your data structure programmatically.
- **Hybrid** gives you the best of both — visual configuration while developing, locked-down code config in production.
You can always change modes later. Start with UI-only if you're exploring.
---
## UI-only mode
This is the default. Run bknd and configure everything through the Admin UI. No setup code required beyond a database connection.
```typescript title="bknd.config.ts"
import type { BkndConfig } from "bknd";
export default {
connection: { url: "file:data.db" },
} satisfies BkndConfig;
```
If you want to provide an initial data structure (entities, auth settings, etc.), pass it via `config`. It will only be applied when the database is empty.
```typescript title="bknd.config.ts"
import type { BkndConfig } from "bknd";
export default {
connection: { url: "file:data.db" },
config: {
auth: { enabled: true },
},
} satisfies BkndConfig;
```
<Callout type="info">
In UI-only mode, the `config` property is only applied on first boot. After that, all changes are made through the Admin UI.
</Callout>
**Next step:** Pick your [framework or runtime integration](/integration/introduction) to wire bknd into your app.
---
## Code-only mode
Define your data structure programmatically with a Drizzle-like API. The Admin UI becomes read-only for configuration — you still use it to manage data.
```typescript title="bknd.config.ts"
import { type BkndConfig, em, entity, text, boolean } from "bknd";
import { secureRandomString } from "bknd/utils";
const schema = em({
todos: entity("todos", {
title: text(),
done: boolean(),
}),
});
export default {
connection: { url: "file:data.db" },
config: {
data: schema.toJSON(),
auth: {
enabled: true,
jwt: { secret: secureRandomString(64) },
},
},
options: {
mode: "code",
},
} satisfies BkndConfig;
```
Unlike UI-only mode, the `config` is applied on every boot. If you change the schema, you may need to [sync the database](/usage/cli#syncing-the-database-sync).
**Next step:** Pick your [framework or runtime integration](/integration/introduction) to wire bknd into your app.
---
## Hybrid mode
Use the Admin UI to configure your backend while developing. When you're ready to deploy, export the config and run in code mode for production.
```typescript title="bknd.config.ts"
import type { BkndConfig } from "bknd";
import appConfig from "./appconfig.json" with { type: "json" };
export default {
connection: { url: "file:data.db" },
config: appConfig,
options: {
mode: process.env.NODE_ENV === "development" ? "db" : "code",
manager: {
secrets: process.env,
},
},
} satisfies BkndConfig;
```
To export your config, secrets, and types, use the CLI or plugins:
| What | CLI Command | Plugin |
|------|-------------|--------|
| Configuration | [`bknd config`](/usage/cli/#getting-the-configuration-config) | [`syncConfig`](/extending/plugins/#syncconfig) |
| Secrets | [`bknd secrets`](/usage/cli/#getting-the-secrets-secrets) | [`syncSecrets`](/extending/plugins/#syncsecrets) |
| Types | [`bknd types`](/usage/cli/#generating-types-types) | [`syncTypes`](/extending/plugins/#synctypes) |
**Next step:** Pick your [framework or runtime integration](/integration/introduction) to wire bknd into your app.
---
## Mode helpers
For code and hybrid modes, bknd provides helper functions that handle syncing, mode switching, and schema validation automatically.
```typescript title="bknd.config.ts (code mode with Bun)"
import { code } from "bknd/modes";
import { type BunBkndConfig, writer } from "bknd/adapter/bun";
export default code<BunBkndConfig>({
connection: { url: "file:data.db" },
writer,
isProduction: Bun.env.NODE_ENV === "production",
typesFilePath: "bknd-types.d.ts",
});
```
```typescript title="bknd.config.ts (hybrid mode with Bun)"
import { hybrid } from "bknd/modes";
import { type BunBkndConfig, writer, reader } from "bknd/adapter/bun";
export default hybrid<BunBkndConfig>({
connection: { url: "file:data.db" },
writer,
reader,
secrets: await Bun.file(".env.local").json(),
isProduction: Bun.env.NODE_ENV === "production",
typesFilePath: "bknd-types.d.ts",
configFilePath: "bknd-config.json",
});
```
Mode helpers give you:
- Built-in syncing of config, types, and secrets
- Automatic schema syncing in development
- Automatic mode switching (db → code) in production for hybrid
- Skipped config validation in production for faster boot
---
## Further reading
- [Framework & runtime integrations](/integration/introduction) — wire bknd into Next.js, Astro, Bun, Cloudflare, etc.
- [Database configuration](/usage/database) — choose and configure your SQL database
- [Configuration reference](/extending/config) — full `BkndConfig` API, plugins, events, and lifecycle hooks
@@ -9,3 +9,7 @@ tags: ["guide"]
[Discord](https://discord.gg/952SFk8Tb8).
</Callout>
<Callout type="warn" title="Set up permissions before enabling the Guard">
If you enable the Guard without first creating an admin role (with `implicit_allow: true`) and attaching it to a user, you will be locked out of the admin portal. See [Securing Your Admin Portal](/modules/auth#securing-your-admin-portal) for the full checklist.
</Callout>
+4
View File
@@ -9,3 +9,7 @@ tags: ["guide"]
[Discord](https://discord.gg/952SFk8Tb8).
</Callout>
<Callout type="warn" title="Set up roles before enabling the Guard">
If you enable the Guard without first creating a role with `implicit_allow: true` and attaching it to a user, you will be locked out of the admin portal. See [Securing Your Admin Portal](/modules/auth#securing-your-admin-portal) for the full checklist.
</Callout>
+5
View File
@@ -9,4 +9,9 @@ export const redirectsConfig = [
destination: "/start",
permanent: true,
},
{
source: "/usage/introduction",
destination: "/usage/setup",
permanent: true,
},
];
+1 -1
View File
@@ -14,7 +14,7 @@
"@astrojs/react": "^4.2.2",
"@types/react": "^19.0.12",
"@types/react-dom": "^19.0.4",
"astro": "^4.16.16",
"astro": "^5.16.9",
"bknd": "file:../../app",
"react": "^19.1.0",
"react-dom": "^19.1.0",
+5 -1
View File
@@ -8,6 +8,7 @@
"node": ">=22.13"
},
"scripts": {
"build": "bun i && bun run --filter bknd build:all",
"updater": "bun x npm-check-updates -ui",
"ci": "find . -name 'node_modules' -type d -exec rm -rf {} + && bun install",
"npm:local": "verdaccio --config verdaccio.yml",
@@ -24,5 +25,8 @@
"typescript": "^5.9.3",
"verdaccio": "^6.2.1"
},
"workspaces": ["app", "packages/*"]
"workspaces": [
"app",
"packages/*"
]
}