diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..ed2e73c
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,267 @@
+# Contributing to bknd
+
+Thank you for your interest in contributing to bknd. This guide will help you get started, understand the codebase, and submit contributions that align with the project's direction.
+
+## Table of Contents
+
+- [Before You Start](#before-you-start)
+- [Important Resources](#important-resources)
+- [Development Setup](#development-setup)
+- [Project Structure](#project-structure)
+- [Understanding the Codebase](#understanding-the-codebase)
+- [Running Tests](#running-tests)
+- [Code Style](#code-style)
+- [Submitting Changes](#submitting-changes)
+- [AI-Generated Code](#ai-generated-code)
+- [Contributing to Documentation](#contributing-to-documentation)
+- [Reporting Bugs](#reporting-bugs)
+- [Getting Help](#getting-help)
+- [Contributors](#contributors)
+- [Maintainers](#maintainers)
+
+## Before You Start
+
+**Open a GitHub Issue before writing code.** This is the preferred way to propose any change. It lets maintainers and the community align on the approach before time is spent on implementation. If you have discussed a contribution in the [Discord server](https://discord.com/invite/952SFk8Tb8) instead, include a link to the relevant Discord thread in your pull request. Pull requests submitted without a corresponding issue or Discord discussion may be closed.
+
+**Unsolicited architectural changes will be closed.** The internal architecture of bknd is intentional. Refactors, restructuring, or changes to core patterns must be discussed and approved before any code is written.
+
+Contributions that are generally welcome (after opening an issue):
+
+- Bug fixes
+- New adapters, strategies, or storage backends
+- Documentation improvements
+- New examples
+- Test coverage improvements
+
+**A note on versioning**: bknd is pre-1.0 and under active development. Full backward compatibility is not guaranteed before v1.0.0. Contributors should be aware that APIs and internal interfaces may change between releases.
+
+## Important Resources
+
+- **Documentation**: https://docs.bknd.io
+- **Issue Tracker**: https://github.com/bknd-io/bknd/issues
+- **Discord**: https://discord.com/invite/952SFk8Tb8
+- **FAQ / Search**: https://www.answeroverflow.com/c/1308395750564302952
+
+## Development Setup
+
+### Prerequisites
+
+- Node.js 22.13 or higher
+- Bun 1.3.3
+
+### Getting Started
+
+1. Fork the repository and clone your fork.
+
+2. Install dependencies from the repo root:
+
+ ```bash
+ bun install
+ ```
+
+3. Navigate to the main app package:
+
+ ```bash
+ cd app
+ ```
+
+4. Copy the example environment file:
+
+ ```bash
+ cp .env.example .env
+ ```
+
+ The default `.env.example` includes a file-based SQLite database (`file:.db/dev.db`). You can change `VITE_DB_URL` to `:memory:` for an in-memory database instead.
+
+5. Start the dev server:
+
+ ```bash
+ bun run dev
+ ```
+
+ The dev server runs on `http://localhost:28623` using Vite with Hono.
+
+## Project Structure
+
+This is a Bun monorepo using workspaces. The vast majority of the code lives in `app/`.
+
+```
+bknd/
+ app/ # Main "bknd" npm package (this is where most work happens)
+ src/
+ adapter/ # Runtime/framework adapters (node, bun, cloudflare, nextjs, astro, etc.)
+ auth/ # Authentication module (strategies, sessions, user pool)
+ cli/ # CLI commands
+ core/ # Shared utilities, event system, drivers, server helpers
+ data/ # Data module (entities, fields, relations, queries, schema)
+ flows/ # Workflow engine and tasks
+ media/ # Media module (storage adapters, uploads)
+ modes/ # Configuration modes (code, hybrid, db)
+ modules/ # Module system, MCP server, permissions framework
+ plugins/ # Built-in plugins (auth, data, cloudflare, dev)
+ ui/ # All frontend code
+ client/ # TypeScript SDK and React hooks (exported as bknd/client)
+ elements/ # React components for auth/media (exported as bknd/elements)
+ (everything else) # Admin UI (exported as bknd/ui)
+ App.ts # Central application orchestrator
+ index.ts # Main package exports
+ __test__/ # Unit tests (mirrors src/ structure)
+ e2e/ # End-to-end tests (Playwright)
+ build.ts # Build script (tsup/esbuild)
+ build.cli.ts # CLI build script
+ packages/ # Small satellite packages
+ cli/ # Standalone CLI package (bknd-cli)
+ plasmic/ # Plasmic integration
+ postgres/ # Postgres helper
+ sqlocal/ # SQLocal helper
+ docs/ # Documentation site (Next.js + Fumadocs, deployed to docs.bknd.io)
+ examples/ # Example projects across runtimes and frameworks
+ docker/ # Docker configuration
+```
+
+## Understanding the Codebase
+
+### Path Aliases
+
+The project uses TypeScript path aliases defined in `app/tsconfig.json`. Imports like `core/utils`, `data/connection`, or `auth/authenticate` resolve to directories under `app/src/`. When reading source code, keep this in mind -- an import like `import { Connection } from "data/connection"` refers to `app/src/data/connection`, not an external package.
+
+### Module System
+
+bknd is built around four core modules, each with its own schema, API routes, and permissions:
+
+- **Data** -- entity definitions, field types, relations, queries (backed by Kysely)
+- **Auth** -- authentication strategies, sessions, user management
+- **Media** -- file storage with pluggable adapters (S3, Cloudinary, local filesystem, etc.)
+- **Flows** -- workflow automation and task execution
+
+These modules are managed by the `ModuleManager` (in `app/src/modules/`), which handles configuration, building, and lifecycle.
+
+### Adapter Pattern
+
+Adapters in `app/src/adapter/` allow bknd to run on different runtimes and frameworks. Each adapter provides the glue between bknd's Hono-based server and a specific environment (Node, Bun, Cloudflare Workers, Next.js, Astro, etc.).
+
+### Plugin System
+
+Plugins (in `app/src/plugins/`) hook into the app lifecycle via callbacks like `beforeBuild`, `onBuilt`, `onServerInit`, and `schema`. They can add entities, register routes, and extend behavior.
+
+### Build System
+
+The build is handled by a custom `app/build.ts` script using tsup (esbuild under the hood). It builds four targets in parallel:
+
+- **API** -- the core backend
+- **UI** -- the admin interface
+- **Elements** -- standalone React components
+- **Adapters** -- all runtime/framework adapters
+
+## Running Tests
+
+All test commands run from the `app/` directory.
+
+| Command | Runner | What it runs |
+|---|---|---|
+| `bun run test` | Bun test | Unit tests (`*.spec.ts` files) |
+| `bun run test:node` | Vitest | Node-specific tests (`*.vi-test.ts`, `*.vitest.ts`) |
+| `bun run test:e2e` | Playwright | End-to-end tests (`*.e2e-spec.ts` in `e2e/`) |
+| `bun run types` | TypeScript | Type checking (no emit) |
+
+CI runs both Bun and Node tests, with a Postgres 17 service for database integration tests.
+
+## Code Style
+
+The project uses **Biome** for formatting and linting.
+
+- 3-space indentation for JavaScript/TypeScript
+- 100-character line width
+- Spaces (not tabs)
+
+To format and lint, run from the repo root:
+
+```bash
+bunx biome format --write ./app
+bunx biome lint --changed --write ./app
+```
+
+Run both before submitting a PR. CI will catch style issues, but it is better to fix them locally first.
+
+## Submitting Changes
+
+1. Open a GitHub Issue describing your proposed change, or link to a Discord thread where it was discussed. Wait for feedback before writing code.
+2. Fork the repo and create a branch from `main`.
+3. Keep changes focused and minimal. One PR per issue.
+4. Add or update tests if applicable.
+5. Run the test suite and linter before pushing.
+6. Open a pull request against `main`. Reference the issue number in the PR description.
+
+Expect a response from maintainers, but be patient -- this is an actively developed project and review may take some time.
+
+## AI-Generated Code
+
+If you use AI tools to write or assist with your code, you must:
+
+- **Fully review all AI-generated code yourself** before submitting. You are responsible for understanding and standing behind every line in your PR.
+- **Include the prompts used** in the PR description. This gives maintainers additional context for reviewing the code.
+
+Pull requests with unreviewed AI-generated code will be closed.
+
+## Contributing to Documentation
+
+The documentation site lives in the `docs/` directory and is a **separate Next.js application** built with [Fumadocs](https://fumadocs.dev). It uses **npm** (not Bun) as its package manager.
+
+### Docs Setup
+
+```bash
+cd docs
+npm install
+npm run dev
+```
+
+The docs dev server runs on `http://localhost:3000`.
+
+### Where Documentation Lives
+
+Documentation content is written in MDX and located in `docs/content/docs/`. The directory is organized into:
+
+- `(documentation)/` -- core documentation pages
+- `guide/` -- guides and tutorials
+- `api-reference/` -- API reference (partially auto-generated from OpenAPI)
+
+To add or edit a page, create or modify an `.mdx` file in the appropriate directory. Page metadata is defined via frontmatter at the top of each file. Navigation order is controlled by `meta.json` files in each directory.
+
+### Building the Docs
+
+```bash
+npm run build
+```
+
+This generates the OpenAPI and MCP reference pages before building the Next.js site. Make sure the build succeeds locally before submitting a docs PR.
+
+## Reporting Bugs
+
+Open a GitHub Issue with:
+
+- A clear title describing the problem.
+- Steps to reproduce the bug.
+- Expected behavior vs. actual behavior.
+- Your environment (runtime, database, adapter, bknd version).
+- Any relevant error messages or logs.
+
+## Getting Help
+
+- **Discord**: https://discord.com/invite/952SFk8Tb8
+- **FAQ / Search**: https://www.answeroverflow.com/c/1308395750564302952
+- **GitHub Issues**: https://github.com/bknd-io/bknd/issues
+
+## Contributors
+
+Thank you to everyone who has contributed to bknd.
+
+
+
+
+
+Made with [contrib.rocks](https://contrib.rocks).
+
+## Maintainers
+
+- **Dennis** ([@dswbx](https://github.com/dswbx)) -- Creator and maintainer of bknd
+- **Cameron Pak** ([@cameronapak](https://github.com/cameronapak)) -- Maintainer of bknd docs
diff --git a/README.md b/README.md
index 2d08d83..a28f54b 100644
--- a/README.md
+++ b/README.md
@@ -165,3 +165,19 @@ npx bknd run
```bash
npm install bknd
```
+
+## Contributing
+
+We welcome contributions! Please read our [CONTRIBUTING.md](CONTRIBUTING.md) before getting started. It covers dev setup, project structure, testing, code style, and how to submit changes.
+
+Before writing code, please open a GitHub Issue or start a conversation in [Discord](https://discord.com/invite/952SFk8Tb8) so we can align on the approach.
+
+## Contributors
+
+Thank you to everyone who has contributed to bknd.
+
+
+
+
+
+Made with [contrib.rocks](https://contrib.rocks).
diff --git a/app/__test__/adapter/adapter.test.ts b/app/__test__/adapter/adapter.test.ts
index 8527b5d..8e8eb99 100644
--- a/app/__test__/adapter/adapter.test.ts
+++ b/app/__test__/adapter/adapter.test.ts
@@ -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 = >(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> = [
+ [{}],
+ [{}, { 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",
- });
+ });
+ });
});
diff --git a/app/build.ts b/app/build.ts
index 3fd35ef..1ab90ea 100644
--- a/app/build.ts
+++ b/app/build.ts
@@ -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);
@@ -14,164 +14,178 @@ const clean = args.includes("--clean");
// silence tsup
const oldConsole = {
- log: console.log,
- warn: console.warn,
+ log: console.log,
+ warn: console.warn,
};
console.log = () => {};
console.warn = () => {};
const define = {
- __isDev: "0",
- __version: JSON.stringify(pkg.version),
+ __isDev: "0",
+ __version: JSON.stringify(pkg.version),
};
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;
function buildTypes() {
- if (types_running || !types) return;
- types_running = true;
+ if (types_running || !types) return;
+ types_running = true;
- Bun.spawn(["bun", "build:types"], {
- stdout: "inherit",
- onExit: () => {
- oldConsole.log(c.cyan("[Types]"), c.green("built"));
- Bun.spawn(["bun", "tsc-alias"], {
- stdout: "inherit",
- onExit: () => {
- oldConsole.log(c.cyan("[Types]"), c.green("aliased"));
- types_running = false;
- },
- });
- },
- });
+ Bun.spawn(["bun", "build:types"], {
+ stdout: "inherit",
+ onExit: () => {
+ oldConsole.log(c.cyan("[Types]"), c.green("built"));
+ Bun.spawn(["bun", "tsc-alias"], {
+ stdout: "inherit",
+ onExit: () => {
+ oldConsole.log(c.cyan("[Types]"), c.green("aliased"));
+ types_running = false;
+ },
+ });
+ },
+ });
}
if (types && !watch) {
- buildTypes();
+ buildTypes();
}
let watcher_timeout: any;
function delayTypes() {
- if (!watch || !types) return;
- if (watcher_timeout) {
- clearTimeout(watcher_timeout);
- }
- watcher_timeout = setTimeout(buildTypes, 1000);
+ if (!watch || !types) return;
+ if (watcher_timeout) {
+ clearTimeout(watcher_timeout);
+ }
+ watcher_timeout = setTimeout(buildTypes, 1000);
}
const dependencies = Object.keys(pkg.dependencies);
// collection of always-external packages
const external = [
- ...dependencies,
- "bun:test",
- "node:test",
- "node:assert/strict",
- "@libsql/client",
- "bknd",
- /^bknd\/.*/,
- "jsonv-ts",
- /^jsonv-ts\/.*/,
+ ...dependencies,
+ "bun:test",
+ "node:test",
+ "node:assert/strict",
+ "@libsql/client",
+ "bknd",
+ /^bknd\/.*/,
+ "jsonv-ts",
+ /^jsonv-ts\/.*/,
] as const;
/**
* Building backend and general API
*/
async function buildApi() {
- await tsup.build({
- minify,
- sourcemap,
- // don't use tsup's broken watch, we'll handle it ourselves
- watch: false,
- define,
- entry: [
- "src/index.ts",
- "src/core/utils/index.ts",
- "src/plugins/index.ts",
- "src/modes/index.ts",
- ],
- outDir: "dist",
- external: [...external],
- metafile: true,
- target: "esnext",
- platform: "browser",
- removeNodeProtocol: false,
- format: ["esm"],
- splitting: false,
- loader: {
- ".svg": "dataurl",
- },
- onSuccess: async () => {
- delayTypes();
- oldConsole.log(c.cyan("[API]"), c.green("built"));
- },
- });
+ await tsup.build({
+ minify,
+ sourcemap,
+ // don't use tsup's broken watch, we'll handle it ourselves
+ watch: false,
+ define,
+ entry: [
+ "src/index.ts",
+ "src/core/utils/index.ts",
+ "src/plugins/index.ts",
+ "src/modes/index.ts",
+ ],
+ outDir: "dist",
+ external: [...external],
+ metafile: true,
+ target: "esnext",
+ platform: "browser",
+ removeNodeProtocol: false,
+ format: ["esm"],
+ splitting: false,
+ loader: {
+ ".svg": "dataurl",
+ },
+ onSuccess: async () => {
+ delayTypes();
+ oldConsole.log(c.cyan("[API]"), c.green("built"));
+ },
+ });
}
async function rewriteClient(path: string) {
- const bundle = await Bun.file(path).text();
- await Bun.write(path, '"use client";\n' + bundle.replaceAll("ui/client", "bknd/client"));
+ const bundle = await Bun.file(path).text();
+ await Bun.write(
+ path,
+ '"use client";\n' + bundle.replaceAll("ui/client", "bknd/client"),
+ );
}
/**
* Building UI for direct imports
*/
async function buildUi() {
- const base = {
- minify,
- sourcemap,
- watch: false,
- define,
- external: [
- ...external,
- "react",
- "react-dom",
- "react/jsx-runtime",
- "react/jsx-dev-runtime",
- "use-sync-external-store",
- /codemirror/,
- "@xyflow/react",
- "@mantine/core",
- ],
- metafile: true,
- platform: "browser",
- format: ["esm"],
- splitting: false,
- bundle: true,
- treeshake: true,
- loader: {
- ".svg": "dataurl",
- },
- esbuildOptions: (options) => {
- options.logLevel = "silent";
- },
- } satisfies tsup.Options;
+ const base = {
+ minify,
+ sourcemap,
+ watch: false,
+ define,
+ external: [
+ ...external,
+ "react",
+ "react-dom",
+ "react/jsx-runtime",
+ "react/jsx-dev-runtime",
+ "use-sync-external-store",
+ /codemirror/,
+ "@xyflow/react",
+ "@mantine/core",
+ ],
+ metafile: true,
+ platform: "browser",
+ format: ["esm"],
+ splitting: false,
+ bundle: true,
+ treeshake: true,
+ loader: {
+ ".svg": "dataurl",
+ },
+ esbuildOptions: (options) => {
+ options.logLevel = "silent";
+ },
+ } satisfies tsup.Options;
- await tsup.build({
- ...base,
- entry: ["src/ui/index.ts", "src/ui/main.css", "src/ui/styles.css"],
- outDir: "dist/ui",
- onSuccess: async () => {
- await rewriteClient("./dist/ui/index.js");
- delayTypes();
- oldConsole.log(c.cyan("[UI]"), c.green("built"));
- },
- });
+ await tsup.build({
+ ...base,
+ entry: ["src/ui/index.ts", "src/ui/main.css", "src/ui/styles.css"],
+ outDir: "dist/ui",
+ onSuccess: async () => {
+ await rewriteClient("./dist/ui/index.js");
+ delayTypes();
+ oldConsole.log(c.cyan("[UI]"), c.green("built"));
+ },
+ });
- await tsup.build({
- ...base,
- entry: ["src/ui/client/index.ts"],
- outDir: "dist/ui/client",
- onSuccess: async () => {
- await rewriteClient("./dist/ui/client/index.js");
- delayTypes();
- oldConsole.log(c.cyan("[UI]"), "Client", c.green("built"));
- },
- });
+ await tsup.build({
+ ...base,
+ entry: ["src/ui/client/index.ts"],
+ outDir: "dist/ui/client",
+ onSuccess: async () => {
+ await rewriteClient("./dist/ui/client/index.js");
+ delayTypes();
+ oldConsole.log(c.cyan("[UI]"), "Client", c.green("built"));
+ },
+ });
}
/**
@@ -180,171 +194,185 @@ async function buildUi() {
* - ui/client is external, and after built replaced with "bknd/client"
*/
async function buildUiElements() {
- await tsup.build({
- minify,
- sourcemap,
- watch: false,
- define,
- entry: ["src/ui/elements/index.ts"],
- outDir: "dist/ui/elements",
- external: [
- "ui/client",
- "bknd",
- /^bknd\/.*/,
- "wouter",
- "react",
- "react-dom",
- "react/jsx-runtime",
- "react/jsx-dev-runtime",
- "use-sync-external-store",
- ],
- metafile: true,
- platform: "browser",
- format: ["esm"],
- splitting: false,
- bundle: true,
- treeshake: true,
- loader: {
- ".svg": "dataurl",
- },
- esbuildOptions: (options) => {
- options.alias = {
- // not important for elements, mock to reduce bundle
- "tailwind-merge": "./src/ui/elements/mocks/tailwind-merge.ts",
- };
- },
- onSuccess: async () => {
- await rewriteClient("./dist/ui/elements/index.js");
- delayTypes();
- oldConsole.log(c.cyan("[UI]"), "Elements", c.green("built"));
- },
- });
+ await tsup.build({
+ minify,
+ sourcemap,
+ watch: false,
+ define,
+ entry: ["src/ui/elements/index.ts"],
+ outDir: "dist/ui/elements",
+ external: [
+ "ui/client",
+ "bknd",
+ /^bknd\/.*/,
+ "wouter",
+ "react",
+ "react-dom",
+ "react/jsx-runtime",
+ "react/jsx-dev-runtime",
+ "use-sync-external-store",
+ ],
+ metafile: true,
+ platform: "browser",
+ format: ["esm"],
+ splitting: false,
+ bundle: true,
+ treeshake: true,
+ loader: {
+ ".svg": "dataurl",
+ },
+ esbuildOptions: (options) => {
+ options.alias = {
+ // not important for elements, mock to reduce bundle
+ "tailwind-merge": "./src/ui/elements/mocks/tailwind-merge.ts",
+ };
+ },
+ onSuccess: async () => {
+ await rewriteClient("./dist/ui/elements/index.js");
+ delayTypes();
+ oldConsole.log(c.cyan("[UI]"), "Elements", c.green("built"));
+ },
+ });
}
/**
* Building adapters
*/
-function baseConfig(adapter: string, overrides: Partial = {}): tsup.Options {
- return {
- minify,
- sourcemap,
- watch: false,
- entry: [`src/adapter/${adapter}/index.ts`],
- format: ["esm"],
- platform: "neutral",
- outDir: `dist/adapter/${adapter}`,
- metafile: true,
- splitting: false,
- removeNodeProtocol: false,
- onSuccess: async () => {
- delayTypes();
- oldConsole.log(c.cyan("[Adapter]"), adapter || "base", c.green("built"));
- },
- ...overrides,
- define: {
- ...define,
- ...overrides.define,
- },
- external: [
- /^cloudflare*/,
- /^@?hono.*?/,
- /^(bknd|react|next|node).*?/,
- /.*\.(html)$/,
- ...external,
- ...(Array.isArray(overrides.external) ? overrides.external : []),
- ],
- };
+function baseConfig(
+ adapter: string,
+ overrides: Partial = {},
+): tsup.Options {
+ return {
+ minify,
+ sourcemap,
+ watch: false,
+ entry: [`src/adapter/${adapter}/index.ts`],
+ format: ["esm"],
+ platform: "neutral",
+ outDir: `dist/adapter/${adapter}`,
+ metafile: true,
+ splitting: false,
+ removeNodeProtocol: false,
+ onSuccess: async () => {
+ delayTypes();
+ oldConsole.log(c.cyan("[Adapter]"), adapter || "base", c.green("built"));
+ },
+ ...overrides,
+ define: {
+ ...define,
+ ...overrides.define,
+ },
+ external: [
+ /^cloudflare*/,
+ /^@?hono.*?/,
+ /^(bknd|react|next|node).*?/,
+ /.*\.(html)$/,
+ ...external,
+ ...(Array.isArray(overrides.external) ? overrides.external : []),
+ ],
+ };
}
async function buildAdapters() {
- await Promise.all([
- // base adapter handles
- tsup.build({
- ...baseConfig(""),
- target: "esnext",
- platform: "neutral",
- entry: ["src/adapter/index.ts"],
- outDir: "dist/adapter",
- // only way to keep @vite-ignore comments
- minify: false,
- }),
+ await Promise.all([
+ // base adapter handles
+ tsup.build({
+ ...baseConfig(""),
+ target: "esnext",
+ platform: "neutral",
+ entry: ["src/adapter/index.ts"],
+ outDir: "dist/adapter",
+ // only way to keep @vite-ignore comments
+ minify: false,
+ }),
- // specific adatpers
- tsup.build(baseConfig("react-router")),
- tsup.build(
- baseConfig("browser", {
- external: [/^sqlocal\/?.*?/, "wouter"],
- }),
- ),
- tsup.build(
- baseConfig("bun", {
- external: [/^bun\:.*/],
- }),
- ),
- tsup.build(baseConfig("astro")),
- tsup.build(baseConfig("aws")),
- tsup.build(
- baseConfig("cloudflare", {
- external: ["wrangler", "node:process"],
- }),
- ),
- tsup.build(
- baseConfig("cloudflare/proxy", {
- target: "esnext",
- entry: ["src/adapter/cloudflare/proxy.ts"],
- outDir: "dist/adapter/cloudflare",
- metafile: false,
- external: [/bknd/, "wrangler", "node:process"],
- }),
- ),
-
- tsup.build({
- ...baseConfig("vite"),
- platform: "node",
+ // specific adatpers
+ tsup.build(baseConfig("react-router")),
+ tsup.build(
+ baseConfig("browser", {
+ external: [/^sqlocal\/?.*?/, "wouter"],
}),
-
- tsup.build({
- ...baseConfig("nextjs"),
- platform: "node",
+ ),
+ tsup.build(
+ baseConfig("bun", {
+ external: [/^bun\:.*/],
}),
-
- tsup.build({
- ...baseConfig("sveltekit"),
- platform: "node",
+ ),
+ tsup.build(baseConfig("astro")),
+ tsup.build(baseConfig("aws")),
+ tsup.build(
+ baseConfig("cloudflare", {
+ external: ["wrangler", "node:process"],
}),
-
- tsup.build({
- ...baseConfig("node"),
- platform: "node",
+ ),
+ tsup.build(
+ baseConfig("cloudflare/proxy", {
+ target: "esnext",
+ entry: ["src/adapter/cloudflare/proxy.ts"],
+ outDir: "dist/adapter/cloudflare",
+ metafile: false,
+ external: [/bknd/, "wrangler", "node:process"],
}),
+ ),
- tsup.build({
- ...baseConfig("sqlite/edge"),
- entry: ["src/adapter/sqlite/edge.ts"],
- outDir: "dist/adapter/sqlite",
- metafile: false,
- }),
+ tsup.build({
+ ...baseConfig("vite"),
+ platform: "node",
+ }),
- tsup.build({
- ...baseConfig("sqlite/node"),
- entry: ["src/adapter/sqlite/node.ts"],
- outDir: "dist/adapter/sqlite",
- platform: "node",
- metafile: false,
- }),
+ tsup.build({
+ ...baseConfig("nextjs"),
+ platform: "node",
+ }),
- tsup.build({
- ...baseConfig("sqlite/bun"),
- entry: ["src/adapter/sqlite/bun.ts"],
- outDir: "dist/adapter/sqlite",
- metafile: false,
- external: [/^bun\:.*/],
- }),
- ]);
+ tsup.build({
+ ...baseConfig("tanstack-start"),
+ platform: "node",
+ }),
+
+ tsup.build({
+ ...baseConfig("sveltekit"),
+ platform: "node",
+ }),
+
+ tsup.build({
+ ...baseConfig("node"),
+ platform: "node",
+ }),
+
+ tsup.build({
+ ...baseConfig("sqlite/edge"),
+ entry: ["src/adapter/sqlite/edge.ts"],
+ outDir: "dist/adapter/sqlite",
+ metafile: false,
+ }),
+
+ tsup.build({
+ ...baseConfig("sqlite/node"),
+ entry: ["src/adapter/sqlite/node.ts"],
+ outDir: "dist/adapter/sqlite",
+ platform: "node",
+ metafile: false,
+ }),
+
+ tsup.build({
+ ...baseConfig("sqlite/bun"),
+ entry: ["src/adapter/sqlite/bun.ts"],
+ outDir: "dist/adapter/sqlite",
+ metafile: false,
+ external: [/^bun\:.*/],
+ }),
+
+ ]);
}
async function buildAll() {
- await Promise.all([buildApi(), buildUi(), buildUiElements(), buildAdapters()]);
+ await Promise.all([
+ buildApi(),
+ buildUi(),
+ buildUiElements(),
+ buildAdapters(),
+ ]);
}
// initial build
@@ -352,39 +380,47 @@ await buildAll();
// custom watcher since tsup's watch is broken in 8.3.5+
if (watch) {
- oldConsole.log(c.cyan("[Watch]"), "watching for changes in src/...");
+ oldConsole.log(c.cyan("[Watch]"), "watching for changes in src/...");
- let debounceTimer: ReturnType | null = null;
- let isBuilding = false;
+ let debounceTimer: ReturnType | null = null;
+ let isBuilding = false;
- const rebuild = async () => {
- if (isBuilding) return;
- isBuilding = true;
- oldConsole.log(c.cyan("[Watch]"), "rebuilding...");
- try {
- await buildAll();
- oldConsole.log(c.cyan("[Watch]"), c.green("done"));
- } catch (e) {
- oldConsole.warn(c.cyan("[Watch]"), c.red("build failed"), e);
- }
- isBuilding = false;
- };
+ const rebuild = async () => {
+ if (isBuilding) return;
+ isBuilding = true;
+ oldConsole.log(c.cyan("[Watch]"), "rebuilding...");
+ try {
+ await buildAll();
+ oldConsole.log(c.cyan("[Watch]"), c.green("done"));
+ } catch (e) {
+ oldConsole.warn(c.cyan("[Watch]"), c.red("build failed"), e);
+ }
+ isBuilding = false;
+ };
- const debouncedRebuild = () => {
- if (debounceTimer) clearTimeout(debounceTimer);
- debounceTimer = setTimeout(rebuild, 100);
- };
+ const debouncedRebuild = () => {
+ if (debounceTimer) clearTimeout(debounceTimer);
+ debounceTimer = setTimeout(rebuild, 100);
+ };
- // watch src directory recursively
- fsWatch(join(import.meta.dir, "src"), { recursive: true }, (event, filename) => {
+ // watch src directory recursively
+ fsWatch(
+ join(import.meta.dir, "src"),
+ { recursive: true },
+ (event, filename) => {
if (!filename) return;
// ignore non-source files
- if (!filename.endsWith(".ts") && !filename.endsWith(".tsx") && !filename.endsWith(".css"))
- return;
+ if (
+ !filename.endsWith(".ts") &&
+ !filename.endsWith(".tsx") &&
+ !filename.endsWith(".css")
+ )
+ return;
oldConsole.log(c.cyan("[Watch]"), c.dim(`${event}: ${filename}`));
debouncedRebuild();
- });
+ },
+ );
- // keep process alive
- await new Promise(() => {});
+ // keep process alive
+ await new Promise(() => {});
}
diff --git a/app/e2e/inc/adapters.ts b/app/e2e/inc/adapters.ts
index 347d23b..30b647b 100644
--- a/app/e2e/inc/adapters.ts
+++ b/app/e2e/inc/adapters.ts
@@ -1,44 +1,47 @@
const adapter = process.env.TEST_ADAPTER;
const default_config = {
- media_adapter: "local",
- base_path: "",
+ media_adapter: "local",
+ base_path: "",
} as const;
const configs = {
- cloudflare: {
- media_adapter: "r2",
- },
- "react-router": {
- base_path: "/admin",
- },
- nextjs: {
- base_path: "/admin",
- },
- astro: {
- base_path: "/admin",
- },
- node: {
- base_path: "",
- },
- bun: {
- base_path: "",
- },
+ cloudflare: {
+ media_adapter: "r2",
+ },
+ "react-router": {
+ base_path: "/admin",
+ },
+ nextjs: {
+ base_path: "/admin",
+ },
+ astro: {
+ base_path: "/admin",
+ },
+ node: {
+ base_path: "",
+ },
+ bun: {
+ base_path: "",
+ },
+ "tanstack-start": {
+ base_path: "/admin",
+ },
};
export function getAdapterConfig(): typeof default_config {
- if (adapter) {
- if (!configs[adapter]) {
- console.warn(
- `Adapter "${adapter}" not found. Available adapters: ${Object.keys(configs).join(", ")}`,
- );
- } else {
- return {
- ...default_config,
- ...configs[adapter],
- };
- }
- }
+ if (adapter) {
+ if (!configs[adapter]) {
+ console.warn(
+ `Adapter "${adapter}" not found. Available adapters: ${Object.keys(configs).join(", ")}`,
+ );
+ } else {
+ return {
+ ...default_config,
+ ...configs[adapter],
+ };
+ }
+ }
- return default_config;
+ return default_config;
}
diff --git a/app/package.json b/app/package.json
index 3027760..ceb3771 100644
--- a/app/package.json
+++ b/app/package.json
@@ -268,6 +268,11 @@
"import": "./dist/adapter/browser/index.js",
"require": "./dist/adapter/browser/index.js"
},
+ "./adapter/tanstack-start": {
+ "types": "./dist/types/adapter/tanstack-start/index.d.ts",
+ "import": "./dist/adapter/tanstack-start/index.js",
+ "require": "./dist/adapter/tanstack-start/index.js"
+ },
"./dist/main.css": "./dist/ui/main.css",
"./dist/styles.css": "./dist/ui/styles.css",
"./dist/manifest.json": "./dist/static/.vite/manifest.json",
@@ -286,6 +291,7 @@
"adapter/bun": ["./dist/types/adapter/bun/index.d.ts"],
"adapter/node": ["./dist/types/adapter/node/index.d.ts"],
"adapter/sveltekit": ["./dist/types/adapter/sveltekit/index.d.ts"],
+ "adapter/tanstack-start": ["./dist/types/adapter/tanstack-start/index.d.ts"],
"adapter/sqlite": ["./dist/types/adapter/sqlite/edge.d.ts"]
}
},
diff --git a/app/src/adapter/tanstack-start/index.ts b/app/src/adapter/tanstack-start/index.ts
new file mode 100644
index 0000000..b6a4846
--- /dev/null
+++ b/app/src/adapter/tanstack-start/index.ts
@@ -0,0 +1 @@
+export * from "./tanstack-start.adapter";
diff --git a/app/src/adapter/tanstack-start/tanstack-start.adapter.spec.ts b/app/src/adapter/tanstack-start/tanstack-start.adapter.spec.ts
new file mode 100644
index 0000000..1101f61
--- /dev/null
+++ b/app/src/adapter/tanstack-start/tanstack-start.adapter.spec.ts
@@ -0,0 +1,16 @@
+import { afterAll, beforeAll, describe } from "bun:test";
+import * as tanstackStart from "./tanstack-start.adapter";
+import { disableConsoleLog, enableConsoleLog } from "core/utils";
+import { adapterTestSuite } from "adapter/adapter-test-suite";
+import { bunTestRunner } from "adapter/bun/test";
+import type { TanstackStartConfig } from "./tanstack-start.adapter";
+
+beforeAll(disableConsoleLog);
+afterAll(enableConsoleLog);
+
+describe("tanstack start adapter", () => {
+ adapterTestSuite(bunTestRunner, {
+ makeApp: tanstackStart.getApp,
+ makeHandler: tanstackStart.serve,
+ });
+});
diff --git a/app/src/adapter/tanstack-start/tanstack-start.adapter.ts b/app/src/adapter/tanstack-start/tanstack-start.adapter.ts
new file mode 100644
index 0000000..55151fe
--- /dev/null
+++ b/app/src/adapter/tanstack-start/tanstack-start.adapter.ts
@@ -0,0 +1,33 @@
+import { createFrameworkApp, type FrameworkBkndConfig } from "bknd/adapter";
+
+export type TanstackStartEnv = NodeJS.ProcessEnv;
+
+export type TanstackStartConfig =
+ FrameworkBkndConfig;
+
+/**
+ * Get bknd app instance
+ * @param config - bknd configuration
+ * @param args - environment variables
+ */
+export async function getApp(
+ config: TanstackStartConfig = {},
+ args: Env = process.env as Env,
+) {
+ return await createFrameworkApp(config, args);
+}
+
+/**
+ * Create request handler for src/routes/api.$.ts
+ * @param config - bknd configuration
+ * @param args - environment variables
+ */
+export function serve(
+ config: TanstackStartConfig = {},
+ args: Env = process.env as Env,
+) {
+ return async (request: Request) => {
+ const app = await getApp(config, args);
+ return app.fetch(request);
+ };
+}
diff --git a/docs/content/docs/(documentation)/extending/admin.mdx b/docs/content/docs/(documentation)/extending/admin.mdx
index a2adede..8b35029 100644
--- a/docs/content/docs/(documentation)/extending/admin.mdx
+++ b/docs/content/docs/(documentation)/extending/admin.mdx
@@ -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
diff --git a/docs/content/docs/(documentation)/extending/config.mdx b/docs/content/docs/(documentation)/extending/config.mdx
index e6b0a4e..369f66a 100644
--- a/docs/content/docs/(documentation)/extending/config.mdx
+++ b/docs/content/docs/(documentation)/extending/config.mdx
@@ -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
{
diff --git a/docs/content/docs/(documentation)/integration/(frameworks)/astro.mdx b/docs/content/docs/(documentation)/integration/(frameworks)/astro.mdx
index be96c3a..8313f7d 100644
--- a/docs/content/docs/(documentation)/integration/(frameworks)/astro.mdx
+++ b/docs/content/docs/(documentation)/integration/(frameworks)/astro.mdx
@@ -170,7 +170,7 @@ export const prerender = false;
theme: "dark",
logo_return_path: "/../"
}}
- client:only
+ client:only="react"
/>