Merge branch 'release/0.20' into feat/add-otp-plugin

This commit is contained in:
dswbx
2025-11-21 20:03:30 +01:00
committed by GitHub
73 changed files with 2049 additions and 94 deletions

View File

@@ -4,7 +4,7 @@
"editor.defaultFormatter": "biomejs.biome",
"editor.codeActionsOnSave": {
//"source.organizeImports.biome": "explicit",
//"source.fixAll.biome": "explicit"
"source.fixAll.biome": "explicit"
},
"typescript.preferences.importModuleSpecifier": "non-relative",
"typescript.preferences.autoImportFileExcludePatterns": [

View File

@@ -0,0 +1,42 @@
import { describe, expect, test } from "bun:test";
import { code, hybrid } from "modes";
describe("modes", () => {
describe("code", () => {
test("verify base configuration", async () => {
const c = code({}) as any;
const config = await c.app?.({} as any);
expect(Object.keys(config)).toEqual(["options"]);
expect(config.options.mode).toEqual("code");
expect(config.options.plugins).toEqual([]);
expect(config.options.manager.skipValidation).toEqual(false);
expect(config.options.manager.onModulesBuilt).toBeDefined();
});
test("keeps overrides", async () => {
const c = code({
connection: {
url: ":memory:",
},
}) as any;
const config = await c.app?.({} as any);
expect(config.connection.url).toEqual(":memory:");
});
});
describe("hybrid", () => {
test("fails if no reader is provided", () => {
// @ts-ignore
expect(hybrid({} as any).app?.({} as any)).rejects.toThrow(/reader/);
});
test("verify base configuration", async () => {
const c = hybrid({ reader: async () => ({}) }) as any;
const config = await c.app?.({} as any);
expect(Object.keys(config)).toEqual(["reader", "beforeBuild", "config", "options"]);
expect(config.options.mode).toEqual("db");
expect(config.options.plugins).toEqual([]);
expect(config.options.manager.skipValidation).toEqual(false);
expect(config.options.manager.onModulesBuilt).toBeDefined();
});
});
});

View File

@@ -43,6 +43,7 @@ export function createHandler<Env = BunEnv>(
export function serve<Env = BunEnv>(
{
app,
distPath,
connection,
config: _config,
@@ -62,6 +63,7 @@ export function serve<Env = BunEnv>(
port,
fetch: createHandler(
{
app,
connection,
config: _config,
options,

View File

@@ -3,7 +3,7 @@
import type { RuntimeBkndConfig } from "bknd/adapter";
import { Hono } from "hono";
import { serveStatic } from "hono/cloudflare-workers";
import type { MaybePromise } from "bknd";
import type { App, MaybePromise } from "bknd";
import { $console } from "bknd/utils";
import { createRuntimeApp } from "bknd/adapter";
import { registerAsyncsExecutionContext, makeConfig, type CloudflareContext } from "./config";
@@ -55,8 +55,12 @@ export async function createApp<Env extends CloudflareEnv = CloudflareEnv>(
// compatiblity
export const getFresh = createApp;
let app: App | undefined;
export function serve<Env extends CloudflareEnv = CloudflareEnv>(
config: CloudflareBkndConfig<Env> = {},
serveOptions?: (args: Env) => {
warm?: boolean;
},
) {
return {
async fetch(request: Request, env: Env, ctx: ExecutionContext) {
@@ -92,8 +96,11 @@ export function serve<Env extends CloudflareEnv = CloudflareEnv>(
}
}
const context = { request, env, ctx } as CloudflareContext<Env>;
const app = await createApp(config, context);
const { warm } = serveOptions?.(env) ?? {};
if (!app || warm !== true) {
const context = { request, env, ctx } as CloudflareContext<Env>;
app = await createApp(config, context);
}
return app.fetch(request, env, ctx);
},

View File

@@ -65,37 +65,31 @@ export function withPlatformProxy<Env extends CloudflareEnv>(
}
return {
...config,
beforeBuild: async (app, registries) => {
if (!use_proxy) return;
const env = await getEnv();
registerMedia(env, registries as any);
await config?.beforeBuild?.(app, registries);
},
bindings: async (env) => {
return (await config?.bindings?.(await getEnv(env))) || {};
},
// @ts-ignore
app: async (_env) => {
const env = await getEnv(_env);
const binding = use_proxy ? getBinding(env, "D1Database") : undefined;
const appConfig = typeof config.app === "function" ? await config.app(env) : config;
const connection =
use_proxy && binding
? d1Sqlite({
binding: binding.value as any,
})
: appConfig.connection;
if (config?.app === undefined && use_proxy && binding) {
return {
connection: d1Sqlite({
binding: binding.value,
}),
};
} else if (typeof config?.app === "function") {
const appConfig = await config?.app(env);
if (binding) {
appConfig.connection = d1Sqlite({
binding: binding.value,
}) as any;
}
return appConfig;
}
return config?.app || {};
return {
...appConfig,
beforeBuild: async (app, registries) => {
if (!use_proxy) return;
const env = await getEnv();
registerMedia(env, registries as any);
await config?.beforeBuild?.(app, registries);
},
bindings: async (env) => {
return (await config?.bindings?.(await getEnv(env))) || {};
},
connection,
};
},
} satisfies CloudflareBkndConfig<Env>;
}

View File

@@ -14,14 +14,15 @@ import type { AdminControllerOptions } from "modules/server/AdminController";
import type { Manifest } from "vite";
export type BkndConfig<Args = any, Additional = {}> = Merge<
CreateAppConfig & {
app?:
| Merge<Omit<BkndConfig, "app"> & Additional>
| ((args: Args) => MaybePromise<Merge<Omit<BkndConfig<Args>, "app"> & Additional>>);
onBuilt?: (app: App) => MaybePromise<void>;
beforeBuild?: (app?: App, registries?: typeof $registries) => MaybePromise<void>;
buildConfig?: Parameters<App["build"]>[0];
} & Additional
CreateAppConfig &
Omit<Additional, "app"> & {
app?:
| Omit<BkndConfig<Args, Additional>, "app">
| ((args: Args) => MaybePromise<Omit<BkndConfig<Args, Additional>, "app">>);
onBuilt?: (app: App) => MaybePromise<void>;
beforeBuild?: (app?: App, registries?: typeof $registries) => MaybePromise<void>;
buildConfig?: Parameters<App["build"]>[0];
}
>;
export type FrameworkBkndConfig<Args = any> = BkndConfig<Args>;

View File

@@ -24,7 +24,7 @@ export async function createApp<Env = NodeEnv>(
path.resolve(distPath ?? relativeDistPath ?? "./node_modules/bknd/dist", "static"),
);
if (relativeDistPath) {
console.warn("relativeDistPath is deprecated, please use distPath instead");
$console.warn("relativeDistPath is deprecated, please use distPath instead");
}
registerLocalMediaAdapter();

View File

@@ -67,7 +67,10 @@ export async function startServer(
$console.info("Server listening on", url);
if (options.open) {
await open(url);
const p = await open(url, { wait: false });
p.on("error", () => {
$console.warn("Couldn't open url in browser");
});
}
}

View File

@@ -4,6 +4,7 @@ import type { KyselyJsonFrom } from "data/relations/EntityRelation";
import type { RepoQuery } from "data/server/query";
import { InvalidSearchParamsException } from "data/errors";
import type { Entity, EntityManager, RepositoryQB } from "data/entities";
import { $console } from "bknd/utils";
export class WithBuilder {
static addClause(
@@ -13,7 +14,7 @@ export class WithBuilder {
withs: RepoQuery["with"],
) {
if (!withs || !isObject(withs)) {
console.warn(`'withs' undefined or invalid, given: ${JSON.stringify(withs)}`);
$console.warn(`'withs' undefined or invalid, given: ${JSON.stringify(withs)}`);
return qb;
}
@@ -37,9 +38,7 @@ export class WithBuilder {
let subQuery = relation.buildWith(entity, ref)(eb);
if (query) {
subQuery = em.repo(other.entity).addOptionsToQueryBuilder(subQuery, query as any, {
ignore: ["with", "join", cardinality === 1 ? "limit" : undefined].filter(
Boolean,
) as any,
ignore: ["with", cardinality === 1 ? "limit" : undefined].filter(Boolean) as any,
});
}
@@ -57,7 +56,7 @@ export class WithBuilder {
static validateWiths(em: EntityManager<any>, entity: string, withs: RepoQuery["with"]) {
let depth = 0;
if (!withs || !isObject(withs)) {
withs && console.warn(`'withs' invalid, given: ${JSON.stringify(withs)}`);
withs && $console.warn(`'withs' invalid, given: ${JSON.stringify(withs)}`);
return depth;
}

View File

@@ -26,7 +26,12 @@ export class JsonSchemaField<
constructor(name: string, config: Partial<JsonSchemaFieldConfig>) {
super(name, config);
this.validator = new Validator({ ...this.getJsonSchema() });
// make sure to hand over clean json
const schema = this.getJsonSchema();
this.validator = new Validator(
typeof schema === "object" ? JSON.parse(JSON.stringify(schema)) : {},
);
}
protected getSchema() {

View File

@@ -10,16 +10,19 @@ export type CodeMode<AdapterConfig extends BkndConfig> = AdapterConfig extends B
? BkndModeConfig<Args, AdapterConfig>
: never;
export function code<Args>(config: BkndCodeModeConfig<Args>): BkndConfig<Args> {
export function code<
Config extends BkndConfig,
Args = Config extends BkndConfig<infer A> ? A : unknown,
>(codeConfig: CodeMode<Config>): BkndConfig<Args> {
return {
...config,
...codeConfig,
app: async (args) => {
const {
config: appConfig,
plugins,
isProd,
syncSchemaOptions,
} = await makeModeConfig(config, args);
} = await makeModeConfig(codeConfig, args);
if (appConfig?.options?.mode && appConfig?.options?.mode !== "code") {
$console.warn("You should not set a different mode than `db` when using code mode");

View File

@@ -1,6 +1,6 @@
import type { BkndConfig } from "bknd/adapter";
import { makeModeConfig, type BkndModeConfig } from "./shared";
import { getDefaultConfig, type MaybePromise, type ModuleConfigs, type Merge } from "bknd";
import { getDefaultConfig, type MaybePromise, type Merge } from "bknd";
import type { DbModuleManager } from "modules/db/DbModuleManager";
import { invariant, $console } from "bknd/utils";
@@ -9,7 +9,7 @@ export type BkndHybridModeOptions = {
* Reader function to read the configuration from the file system.
* This is required for hybrid mode to work.
*/
reader?: (path: string) => MaybePromise<string>;
reader?: (path: string) => MaybePromise<string | object>;
/**
* Provided secrets to be merged into the configuration
*/
@@ -23,8 +23,12 @@ export type HybridMode<AdapterConfig extends BkndConfig> = AdapterConfig extends
? BkndModeConfig<Args, Merge<BkndHybridModeOptions & AdapterConfig>>
: never;
export function hybrid<Args>(hybridConfig: HybridBkndConfig<Args>): BkndConfig<Args> {
export function hybrid<
Config extends BkndConfig,
Args = Config extends BkndConfig<infer A> ? A : unknown,
>(hybridConfig: HybridMode<Config>): BkndConfig<Args> {
return {
...hybridConfig,
app: async (args) => {
const {
config: appConfig,
@@ -40,16 +44,15 @@ export function hybrid<Args>(hybridConfig: HybridBkndConfig<Args>): BkndConfig<A
}
invariant(
typeof appConfig.reader === "function",
"You must set the `reader` option when using hybrid mode",
"You must set a `reader` option when using hybrid mode",
);
let fileConfig: ModuleConfigs;
try {
fileConfig = JSON.parse(await appConfig.reader!(configFilePath)) as ModuleConfigs;
} catch (e) {
const defaultConfig = (appConfig.config ?? getDefaultConfig()) as ModuleConfigs;
await appConfig.writer!(configFilePath, JSON.stringify(defaultConfig, null, 2));
fileConfig = defaultConfig;
const fileContent = await appConfig.reader?.(configFilePath);
let fileConfig = typeof fileContent === "string" ? JSON.parse(fileContent) : fileContent;
if (!fileConfig) {
$console.warn("No config found, using default config");
fileConfig = getDefaultConfig();
await appConfig.writer?.(configFilePath, JSON.stringify(fileConfig, null, 2));
}
return {

View File

@@ -1,7 +1,7 @@
import type { AppPlugin, BkndConfig, MaybePromise, Merge } from "bknd";
import { syncTypes, syncConfig } from "bknd/plugins";
import { syncSecrets } from "plugins/dev/sync-secrets.plugin";
import { invariant, $console } from "bknd/utils";
import { $console } from "bknd/utils";
export type BkndModeOptions = {
/**
@@ -56,6 +56,14 @@ export type BkndModeConfig<Args = any, Additional = {}> = BkndConfig<
Merge<BkndModeOptions & Additional>
>;
function _isProd() {
try {
return process.env.NODE_ENV === "production";
} catch (_e) {
return false;
}
}
export async function makeModeConfig<
Args = any,
Config extends BkndModeConfig<Args> = BkndModeConfig<Args>,
@@ -69,25 +77,24 @@ export async function makeModeConfig<
if (typeof config.isProduction !== "boolean") {
$console.warn(
"You should set `isProduction` option when using managed modes to prevent accidental issues",
"You should set `isProduction` option when using managed modes to prevent accidental issues with writing plugins and syncing schema. As fallback, it is set to",
_isProd(),
);
}
invariant(
typeof config.writer === "function",
"You must set the `writer` option when using managed modes",
);
let needsWriter = false;
const { typesFilePath, configFilePath, writer, syncSecrets: syncSecretsOptions } = config;
const isProd = config.isProduction;
const isProd = config.isProduction ?? _isProd();
const plugins = appConfig?.options?.plugins ?? ([] as AppPlugin[]);
const syncFallback = typeof config.syncSchema === "boolean" ? config.syncSchema : !isProd;
const syncSchemaOptions =
typeof config.syncSchema === "object"
? config.syncSchema
: {
force: config.syncSchema !== false,
drop: true,
force: syncFallback,
drop: syncFallback,
};
if (!isProd) {
@@ -95,6 +102,7 @@ export async function makeModeConfig<
if (plugins.some((p) => p.name === "bknd-sync-types")) {
throw new Error("You have to unregister the `syncTypes` plugin");
}
needsWriter = true;
plugins.push(
syncTypes({
enabled: true,
@@ -114,6 +122,7 @@ export async function makeModeConfig<
if (plugins.some((p) => p.name === "bknd-sync-config")) {
throw new Error("You have to unregister the `syncConfig` plugin");
}
needsWriter = true;
plugins.push(
syncConfig({
enabled: true,
@@ -142,6 +151,7 @@ export async function makeModeConfig<
.join(".");
}
needsWriter = true;
plugins.push(
syncSecrets({
enabled: true,
@@ -174,6 +184,10 @@ export async function makeModeConfig<
}
}
if (needsWriter && typeof config.writer !== "function") {
$console.warn("You must set a `writer` function, attempts to write will fail");
}
return {
config,
isProd,

View File

@@ -223,7 +223,7 @@ export class ModuleManager {
}
extractSecrets() {
const moduleConfigs = structuredClone(this.configs());
const moduleConfigs = JSON.parse(JSON.stringify(this.configs()));
const secrets = { ...this.options?.secrets };
const extractedKeys: string[] = [];

View File

@@ -105,7 +105,10 @@ export class AppServer extends Module<AppServerConfig> {
if (err instanceof Error) {
if (isDebug()) {
return c.json({ error: err.message, stack: err.stack }, 500);
return c.json(
{ error: err.message, stack: err.stack?.split("\n").map((line) => line.trim()) },
500,
);
}
}

View File

@@ -8,7 +8,7 @@ import type {
ModuleApi,
} from "bknd";
import { objectTransform, encodeSearch } from "bknd/utils";
import type { Insertable, Selectable, Updateable } from "kysely";
import type { Insertable, Selectable, Updateable, Generated } from "kysely";
import useSWR, { type SWRConfiguration, type SWRResponse, mutate } from "swr";
import { type Api, useApi } from "ui/client";
@@ -33,6 +33,7 @@ interface UseEntityReturn<
Entity extends keyof DB | string,
Id extends PrimaryFieldType | undefined,
Data = Entity extends keyof DB ? DB[Entity] : EntityData,
ActualId = Data extends { id: infer I } ? (I extends Generated<infer T> ? T : I) : never,
Response = ResponseObject<RepositoryResult<Selectable<Data>>>,
> {
create: (input: Insertable<Data>) => Promise<Response>;
@@ -42,9 +43,11 @@ interface UseEntityReturn<
ResponseObject<RepositoryResult<Id extends undefined ? Selectable<Data>[] : Selectable<Data>>>
>;
update: Id extends undefined
? (input: Updateable<Data>, id: Id) => Promise<Response>
? (input: Updateable<Data>, id: ActualId) => Promise<Response>
: (input: Updateable<Data>) => Promise<Response>;
_delete: Id extends undefined ? (id: Id) => Promise<Response> : () => Promise<Response>;
_delete: Id extends undefined
? (id: PrimaryFieldType) => Promise<Response>
: () => Promise<Response>;
}
export const useEntity = <

View File

@@ -70,8 +70,9 @@ switch (dbType) {
if (example) {
const name = slugify(example);
configPath = `.configs/${slugify(example)}.wrangler.json`;
const exists = await readFile(configPath, "utf-8");
if (!exists) {
try {
await readFile(configPath, "utf-8");
} catch (_e) {
wranglerConfig.name = name;
wranglerConfig.d1_databases[0]!.database_name = name;
wranglerConfig.d1_databases[0]!.database_id = crypto.randomUUID();

View File

@@ -0,0 +1,14 @@
FROM alpine:latest
# Install Node.js and npm
RUN apk add --no-cache nodejs npm
# Set working directory
WORKDIR /app
# Create package.json with type: module
RUN echo '{"type":"module"}' > package.json
# Keep container running (can be overridden)
CMD ["sh"]

12
docker/debug/run-minimal.sh Executable file
View File

@@ -0,0 +1,12 @@
#!/bin/bash
# Build the minimal Alpine image with Node.js
docker build -f Dockerfile.minimal -t bknd-minimal .
# Run the container with the whole app/src directory mapped
docker run -it --rm \
-v "$(pwd)/../app:/app/app" \
-w /app \
-p 1337:1337 \
bknd-minimal

View File

@@ -40,12 +40,6 @@ bun add bknd
</Tabs>
<Callout type="info">
The guide below assumes you're using Astro v4. We've experienced issues with
Astro DB using v5, see [this
issue](https://github.com/withastro/astro/issues/12474).
</Callout>
For the Astro integration to work, you also need to [add the react integration](https://docs.astro.build/en/guides/integrations-guide/react/):
```bash
@@ -159,7 +153,7 @@ Create a new catch-all route at `src/pages/admin/[...admin].astro`:
import { Admin } from "bknd/ui";
import "bknd/dist/styles.css";
import { getApi } from "bknd/adapter/astro";
import { getApi } from "../../../bknd.ts"; // /src/bknd.ts
const api = await getApi(Astro, { mode: "dynamic" });
const user = api.getUser();

View File

@@ -213,9 +213,9 @@ To use it, you have to wrap your configuration in a mode helper, e.g. for `code`
import { code, type CodeMode } from "bknd/modes";
import { type BunBkndConfig, writer } from "bknd/adapter/bun";
const config = {
export default code<BunBkndConfig>({
// some normal bun bknd config
connection: { url: "file:test.db" },
connection: { url: "file:data.db" },
// ...
// a writer is required, to sync the types
writer,
@@ -227,9 +227,7 @@ const config = {
force: true,
drop: true,
}
} satisfies CodeMode<BunBkndConfig>;
export default code(config);
});
```
Similarily, for `hybrid` mode:
@@ -238,9 +236,9 @@ Similarily, for `hybrid` mode:
import { hybrid, type HybridMode } from "bknd/modes";
import { type BunBkndConfig, writer, reader } from "bknd/adapter/bun";
const config = {
export default hybrid<BunBkndConfig>({
// some normal bun bknd config
connection: { url: "file:test.db" },
connection: { url: "file:data.db" },
// ...
// reader/writer are required, to sync the types and config
writer,
@@ -262,7 +260,5 @@ const config = {
force: true,
drop: true,
},
} satisfies HybridMode<BunBkndConfig>;
export default hybrid(config);
});
```

1
examples/.gitignore vendored
View File

@@ -3,3 +3,4 @@
*/deno.lock
*/node_modules
*/*.db
*/worker-configuration.d.ts

View File

@@ -0,0 +1 @@
JWT_SECRET=secret

View File

@@ -0,0 +1,348 @@
# bknd starter: Cloudflare Vite Code-Only
A fullstack React + Vite application with bknd integration, showcasing **code-only mode** and Cloudflare Workers deployment.
## Key Features
This example demonstrates a minimal, code-first approach to building with bknd:
### 💻 Code-Only Mode
Define your entire backend **programmatically** using a Drizzle-like API. Your data structure, authentication, and configuration live directly in code with zero build-time tooling required. Perfect for developers who prefer traditional code-first workflows.
### 🎯 Minimal Boilerplate
Unlike the hybrid mode template, this example uses **no automatic type generation**, **no filesystem plugins**, and **no auto-synced configuration files**. This simulates a typical development environment where you manage types generation manually. If you prefer automatic type generation, you can easily add it using the [CLI](https://docs.bknd.io/usage/cli#generating-types-types) or [Vite plugin](https://docs.bknd.io/extending/plugins#synctypes).
### ⚡ Split Configuration Pattern
- **`config.ts`**: Main configuration that defines your schema and can be safely imported in your worker
- **`bknd.config.ts`**: Wraps the configuration with `withPlatformProxy` for CLI usage with Cloudflare bindings (should NOT be imported in your worker)
This pattern prevents bundling `wrangler` into your worker while still allowing CLI access to Cloudflare resources.
## Project Structure
Inside of your project, you'll see the following folders and files:
```text
/
├── src/
│ ├── app/ # React frontend application
│ │ ├── App.tsx
│ │ ├── routes/
│ │ │ ├── admin.tsx # bknd Admin UI route
│ │ │ └── home.tsx # Example frontend route
│ │ └── main.tsx
│ └── worker/
│ └── index.ts # Cloudflare Worker entry
├── config.ts # bknd configuration with schema definition
├── bknd.config.ts # CLI configuration with platform proxy
├── seed.ts # Optional: seed data for development
├── vite.config.ts # Standard Vite config (no bknd plugins)
├── package.json
└── wrangler.json # Cloudflare Workers configuration
```
## Cloudflare Resources
- **D1:** `wrangler.json` declares a `DB` binding. In production, replace `database_id` with your own (`wrangler d1 create <name>`).
- **R2:** Optional `BUCKET` binding is pre-configured to show how to add additional services.
- **Environment awareness:** `ENVIRONMENT` variable determines whether to sync the database schema automatically (development only).
- **Static assets:** The Assets binding points to `dist/client`. Run `npm run build` before `wrangler deploy` to upload the client bundle alongside the worker.
## Admin UI & Frontend
- `/admin` mounts `<Admin />` from `bknd/ui` with `withProvider={{ user }}` so it respects the authenticated user returned by `useAuth`.
- `/` showcases `useEntityQuery("todos")`, mutation helpers, and authentication state — demonstrating how manually declared types flow into the React code.
## Configuration Files
### `config.ts`
The main configuration file that uses the `code()` mode helper:
```typescript
import type { CloudflareBkndConfig } from "bknd/adapter/cloudflare";
import { code } from "bknd/modes";
import { boolean, em, entity, text } from "bknd";
// define your schema using a Drizzle-like API
const schema = em({
todos: entity("todos", {
title: text(),
done: boolean(),
}),
});
// register your schema for type completion (optional)
// alternatively, you can use the CLI to auto-generate types
type Database = (typeof schema)["DB"];
declare module "bknd" {
interface DB extends Database {}
}
export default code<CloudflareBkndConfig>({
app: (env) => ({
config: {
// convert schema to JSON format
data: schema.toJSON(),
auth: {
enabled: true,
jwt: {
// secrets are directly passed to the config
secret: env.JWT_SECRET,
issuer: "cloudflare-vite-code-example",
},
},
},
// disable the built-in admin controller (we render our own app)
adminOptions: false,
// determines whether the database should be automatically synced
isProduction: env.ENVIRONMENT === "production",
}),
});
```
Key differences from hybrid mode:
- **No auto-generated files**: No `bknd-config.json`, `bknd-types.d.ts`, or `.env.example`
- **Manual type declaration**: Types are declared inline using `declare module "bknd"`
- **Direct secret access**: Secrets come directly from `env` parameters
- **Simpler setup**: No filesystem plugins or readers/writers needed
If you prefer automatic type generation, you can add it later using:
- **CLI**: `npm run bknd -- types` (requires adding `typesFilePath` to config)
- **Plugin**: Import `syncTypes` plugin and configure it in your app
### `bknd.config.ts`
Wraps the configuration for CLI usage with Cloudflare bindings:
```typescript
import { withPlatformProxy } from "bknd/adapter/cloudflare/proxy";
import config from "./config.ts";
export default withPlatformProxy(config, {
useProxy: true,
});
```
**Important**: Don't import this file in your worker, as it would bundle `wrangler` into your production code. This file is only used by the bknd CLI.
### `vite.config.ts`
Standard Vite configuration without bknd-specific plugins:
```typescript
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { cloudflare } from "@cloudflare/vite-plugin";
import tailwindcss from "@tailwindcss/vite";
export default defineConfig({
plugins: [react(), tailwindcss(), cloudflare()],
});
```
## Commands
All commands are run from the root of the project, from a terminal:
| Command | Action |
|:-------------------|:----------------------------------------------------------|
| `npm install` | Installs dependencies, generates types, and seeds database|
| `npm run dev` | Starts local dev server with Vite at `localhost:5173` |
| `npm run build` | Builds the application for production |
| `npm run preview` | Builds and previews the production build locally |
| `npm run deploy` | Builds, syncs the schema and deploys to Cloudflare Workers|
| `npm run bknd` | Runs bknd CLI commands |
| `npm run bknd:seed`| Seeds the database with example data |
| `npm run cf:types` | Generates Cloudflare Worker types from `wrangler.json` |
| `npm run check` | Type checks and does a dry-run deployment |
## Development Workflow
1. **Install dependencies:**
```sh
npm install
```
This will install dependencies, generate Cloudflare types, and seed the database.
2. **Start development server:**
```sh
npm run dev
```
3. **Define your schema in code** (`config.ts`):
```typescript
const schema = em({
todos: entity("todos", {
title: text(),
done: boolean(),
}),
});
```
4. **Manually declare types** (optional, but recommended for IDE support):
```typescript
type Database = (typeof schema)["DB"];
declare module "bknd" {
interface DB extends Database {}
}
```
5. **Use the Admin UI** at `http://localhost:5173/admin` to:
- View and manage your data
- Monitor authentication
- Access database tools
Note: In code mode, you cannot edit the schema through the UI. All schema changes must be done in `config.ts`.
6. **Sync schema changes** to your database:
```sh
# Local development (happens automatically on startup)
npm run dev
# Production database (safe operations only)
CLOUDFLARE_ENV=production npm run bknd -- sync --force
```
## Before You Deploy
### 1. Create a D1 Database
Create a database in your Cloudflare account:
```sh
npx wrangler d1 create my-database
```
Update `wrangler.json` with your database ID:
```json
{
"d1_databases": [
{
"binding": "DB",
"database_name": "my-database",
"database_id": "your-database-id-here"
}
]
}
```
### 2. Set Required Secrets
Set your secrets in Cloudflare Workers:
```sh
# JWT secret (required for authentication)
npx wrangler secret put JWT_SECRET
```
You can generate a secure secret using:
```sh
# Using openssl
openssl rand -base64 64
```
## Deployment
Deploy to Cloudflare Workers:
```sh
npm run deploy
```
This will:
1. Set `ENVIRONMENT=production` to prevent automatic schema syncing
2. Build the Vite application
3. Sync the database schema (safe operations only)
4. Deploy to Cloudflare Workers using Wrangler
In production, bknd will:
- Use the configuration defined in `config.ts`
- Skip config validation for better performance
- Expect secrets to be provided via environment variables
## How Code Mode Works
1. **Define Schema:** Create entities and fields using the Drizzle-like API in `config.ts`
2. **Convert to JSON:** Use `schema.toJSON()` to convert your schema to bknd's configuration format
3. **Manual Types:** Optionally declare types inline for IDE support and type safety
4. **Deploy:** Same configuration runs in both development and production
### Code Mode vs Hybrid Mode
| Feature | Code Mode | Hybrid Mode |
|---------|-----------|-------------|
| Schema Definition | Code-only (`em`, `entity`, `text`) | Visual UI in dev, code in prod |
| Configuration Files | None (all in code) | Auto-generated `bknd-config.json` |
| Type Generation | Manual or opt-in | Automatic |
| Setup Complexity | Minimal | Requires plugins & filesystem access |
| Use Case | Traditional code-first workflows | Rapid prototyping, visual development |
## Type Generation (Optional)
This example intentionally **does not use automatic type generation** to simulate a typical development environment where types are managed manually. This approach:
- Reduces build complexity
- Eliminates dependency on build-time tooling
- Works in any environment without special plugins
However, if you prefer automatic type generation, you can easily add it:
### Option 1: Using the Vite Plugin and `code` helper presets
Add `typesFilePath` to your config:
```typescript
export default code({
typesFilePath: "./bknd-types.d.ts",
// ... rest of config
});
```
For Cloudflare Workers, you'll need the `devFsVitePlugin`:
```typescript
// vite.config.ts
import { devFsVitePlugin } from "bknd/adapter/cloudflare";
export default defineConfig({
plugins: [
// ...
devFsVitePlugin({ configFile: "config.ts" })
],
});
```
Finally, add the generated types to your `tsconfig.json`:
```json
{
"compilerOptions": {
"types": ["./bknd-types.d.ts"]
}
}
```
This provides filesystem access for auto-syncing types despite Cloudflare's `unenv` restrictions.
### Option 2: Using the CLI
You may also use the CLI to generate types:
```sh
npx bknd types --outfile ./bknd-types.d.ts
```
## Database Seeding
Unlike UI-only and hybrid modes where bknd can automatically detect an empty database (by attempting to fetch the configuration. A "table not found" error indicates a fresh database), **code mode requires manual seeding**. This is because in code mode, the configuration is always provided from code, so bknd can't determine if the database is empty without additional queries, which would impact performance.
This example includes a [`seed.ts`](./seed.ts) file that you can run manually. For Cloudflare, it uses `bknd.config.ts` (with `withPlatformProxy`) to access Cloudflare resources like D1 during CLI execution:
```sh
npm run bknd:seed
```
The seed script manually checks if the database is empty before inserting data. See the [seed.ts](./seed.ts) file for implementation details.
## Want to Learn More?
- [Cloudflare Integration Documentation](https://docs.bknd.io/integration/cloudflare)
- [Code Mode Guide](https://docs.bknd.io/usage/introduction#code-only-mode)
- [Mode Helpers Documentation](https://docs.bknd.io/usage/introduction#mode-helpers)
- [Data Structure & Schema API](https://docs.bknd.io/usage/database#data-structure)
- [Discord Community](https://discord.gg/952SFk8Tb8)

View File

@@ -0,0 +1,15 @@
/**
* This file gets automatically picked up by the bknd CLI. Since we're using cloudflare,
* we want to use cloudflare bindings (such as the database). To do this, we need to wrap
* the configuration with the `withPlatformProxy` helper function.
*
* Don't import this file directly in your app, otherwise "wrangler" will be bundled with your worker.
* That's why we split the configuration into two files: `bknd.config.ts` and `config.ts`.
*/
import { withPlatformProxy } from "bknd/adapter/cloudflare/proxy";
import config from "./config.ts";
export default withPlatformProxy(config, {
useProxy: true,
});

View File

@@ -0,0 +1,43 @@
/// <reference types="./worker-configuration.d.ts" />
import type { CloudflareBkndConfig } from "bknd/adapter/cloudflare";
import { code } from "bknd/modes";
import { boolean, em, entity, text } from "bknd";
const schema = em({
todos: entity("todos", {
title: text(),
done: boolean(),
}),
});
// register your schema to get automatic type completion
// alternatively, you can use the CLI to generate types
// learn more at https://docs.bknd.io/usage/cli/#generating-types-types
type Database = (typeof schema)["DB"];
declare module "bknd" {
interface DB extends Database {}
}
export default code<CloudflareBkndConfig>({
app: (env) => ({
config: {
data: schema.toJSON(),
auth: {
enabled: true,
jwt: {
// unlike hybrid mode, secrets are directly passed to the config
secret: env.JWT_SECRET,
issuer: "cloudflare-vite-code-example",
},
},
},
// we need to disable the admin controller using the vite plugin, since we want to render our own app
adminOptions: false,
// this is important to determine whether the database should be automatically synced
isProduction: env.ENVIRONMENT === "production",
// note: usually you would use `options.seed` to seed the database, but since we're using code mode,
// we don't know when the db is empty. So we need to create a separate seed function, see `seed.ts`.
}),
});

View File

@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>bknd + Vite + Cloudflare + React + TS</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/app/main.tsx"></script>
</body>
</html>

View File

@@ -0,0 +1,36 @@
{
"name": "cloudflare-vite-fullstack-bknd-code",
"description": "A template for building a React application with Vite, Cloudflare Workers, and bknd",
"private": true,
"type": "module",
"scripts": {
"build": "vite build",
"cf:types": "wrangler types",
"bknd": "node --experimental-strip-types node_modules/.bin/bknd",
"bknd:seed": "NODE_NO_WARNINGS=1 node --experimental-strip-types seed.ts",
"check": "tsc && vite build && wrangler deploy --dry-run",
"deploy": "CLOUDFLARE_ENV=production vite build && CLOUDFLARE_ENV=production npm run bknd -- sync --force && wrangler deploy",
"dev": "vite",
"preview": "npm run build && vite preview",
"postinstall": "npm run cf:types && npm run bknd:seed"
},
"dependencies": {
"@tailwindcss/vite": "^4.1.17",
"bknd": "file:../../app",
"hono": "4.10.6",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"tailwindcss": "^4.1.17",
"wouter": "^3.7.1"
},
"devDependencies": {
"@cloudflare/vite-plugin": "1.15.2",
"@types/node": "^24.10.1",
"@types/react": "19.2.6",
"@types/react-dom": "19.2.3",
"@vitejs/plugin-react": "5.1.1",
"typescript": "5.9.3",
"vite": "^7.2.4",
"wrangler": "^4.50.0"
}
}

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1,28 @@
/// <reference types="./worker-configuration.d.ts" />
import { createFrameworkApp } from "bknd/adapter";
import config from "./bknd.config.ts";
const app = await createFrameworkApp(config, {});
const {
data: { count: usersCount },
} = await app.em.repo("users").count();
const {
data: { count: todosCount },
} = await app.em.repo("todos").count();
// only run if the database is empty
if (usersCount === 0 && todosCount === 0) {
await app.em.mutator("todos").insertMany([
{ title: "Learn bknd", done: true },
{ title: "Build something cool", done: false },
]);
await app.module.auth.createUser({
email: "test@bknd.io",
password: "12345678",
});
}
process.exit(0);

View File

@@ -0,0 +1,39 @@
import { Router, Switch, Route } from "wouter";
import Home from "./routes/home.tsx";
import { lazy, Suspense, useEffect, useState } from "react";
const Admin = lazy(() => import("./routes/admin.tsx"));
import { useAuth } from "bknd/client";
export default function App() {
const auth = useAuth();
const [verified, setVerified] = useState(false);
useEffect(() => {
auth.verify().then(() => setVerified(true));
}, []);
if (!verified) return null;
return (
<Router>
<Switch>
<Route path="/" component={Home} />
<Route path="/admin/*?">
<Suspense>
<Admin
config={{
basepath: "/admin",
logo_return_path: "/../",
}}
/>
</Suspense>
</Route>
<Route path="*">
<div className="w-full min-h-full flex justify-center items-center font-mono text-4xl">
404
</div>
</Route>
</Switch>
</Router>
);
}

View File

@@ -0,0 +1,14 @@
<svg
width="578"
height="188"
viewBox="0 0 578 188"
fill="black"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M41.5 34C37.0817 34 33.5 37.5817 33.5 42V146C33.5 150.418 37.0817 154 41.5 154H158.5C162.918 154 166.5 150.418 166.5 146V42C166.5 37.5817 162.918 34 158.5 34H41.5ZM123.434 113.942C124.126 111.752 124.5 109.42 124.5 107C124.5 94.2975 114.203 84 101.5 84C99.1907 84 96.9608 84.3403 94.8579 84.9736L87.2208 65.1172C90.9181 63.4922 93.5 59.7976 93.5 55.5C93.5 49.701 88.799 45 83 45C77.201 45 72.5 49.701 72.5 55.5C72.5 61.299 77.201 66 83 66C83.4453 66 83.8841 65.9723 84.3148 65.9185L92.0483 86.0256C87.1368 88.2423 83.1434 92.1335 80.7957 96.9714L65.4253 91.1648C65.4746 90.7835 65.5 90.3947 65.5 90C65.5 85.0294 61.4706 81 56.5 81C51.5294 81 47.5 85.0294 47.5 90C47.5 94.9706 51.5294 99 56.5 99C60.0181 99 63.0648 96.9814 64.5449 94.0392L79.6655 99.7514C78.9094 102.03 78.5 104.467 78.5 107C78.5 110.387 79.2321 113.603 80.5466 116.498L69.0273 123.731C67.1012 121.449 64.2199 120 61 120C55.201 120 50.5 124.701 50.5 130.5C50.5 136.299 55.201 141 61 141C66.799 141 71.5 136.299 71.5 130.5C71.5 128.997 71.1844 127.569 70.6158 126.276L81.9667 119.149C86.0275 125.664 93.2574 130 101.5 130C110.722 130 118.677 124.572 122.343 116.737L132.747 120.899C132.585 121.573 132.5 122.276 132.5 123C132.5 127.971 136.529 132 141.5 132C146.471 132 150.5 127.971 150.5 123C150.5 118.029 146.471 114 141.5 114C138.32 114 135.525 115.649 133.925 118.139L123.434 113.942Z"
/>
<path d="M243.9 151.5C240.4 151.5 237 151 233.7 150C230.4 149 227.4 147.65 224.7 145.95C222 144.15 219.75 142.15 217.95 139.95C216.15 137.65 215 135.3 214.5 132.9L219.3 131.1L218.25 149.7H198.15V39H219.45V89.25L215.4 87.6C216 85.2 217.15 82.9 218.85 80.7C220.55 78.4 222.7 76.4 225.3 74.7C227.9 72.9 230.75 71.5 233.85 70.5C236.95 69.5 240.15 69 243.45 69C250.35 69 256.5 70.8 261.9 74.4C267.3 77.9 271.55 82.75 274.65 88.95C277.85 95.15 279.45 102.25 279.45 110.25C279.45 118.25 277.9 125.35 274.8 131.55C271.7 137.75 267.45 142.65 262.05 146.25C256.75 149.75 250.7 151.5 243.9 151.5ZM238.8 133.35C242.8 133.35 246.25 132.4 249.15 130.5C252.15 128.5 254.5 125.8 256.2 122.4C257.9 118.9 258.75 114.85 258.75 110.25C258.75 105.75 257.9 101.75 256.2 98.25C254.6 94.75 252.3 92.05 249.3 90.15C246.3 88.25 242.8 87.3 238.8 87.3C234.8 87.3 231.3 88.25 228.3 90.15C225.3 92.05 222.95 94.75 221.25 98.25C219.55 101.75 218.7 105.75 218.7 110.25C218.7 114.85 219.55 118.9 221.25 122.4C222.95 125.8 225.3 128.5 228.3 130.5C231.3 132.4 234.8 133.35 238.8 133.35ZM308.312 126.15L302.012 108.6L339.512 70.65H367.562L308.312 126.15ZM288.062 150V39H309.362V150H288.062ZM341.762 150L313.262 114.15L328.262 102.15L367.412 150H341.762ZM371.675 150V70.65H392.075L392.675 86.85L388.475 88.65C389.575 85.05 391.525 81.8 394.325 78.9C397.225 75.9 400.675 73.5 404.675 71.7C408.675 69.9 412.875 69 417.275 69C423.275 69 428.275 70.2 432.275 72.6C436.375 75 439.425 78.65 441.425 83.55C443.525 88.35 444.575 94.3 444.575 101.4V150H423.275V103.05C423.275 99.45 422.775 96.45 421.775 94.05C420.775 91.65 419.225 89.9 417.125 88.8C415.125 87.6 412.625 87.1 409.625 87.3C407.225 87.3 404.975 87.7 402.875 88.5C400.875 89.2 399.125 90.25 397.625 91.65C396.225 93.05 395.075 94.65 394.175 96.45C393.375 98.25 392.975 100.2 392.975 102.3V150H382.475C380.175 150 378.125 150 376.325 150C374.525 150 372.975 150 371.675 150ZM488.536 151.5C481.636 151.5 475.436 149.75 469.936 146.25C464.436 142.65 460.086 137.8 456.886 131.7C453.786 125.5 452.236 118.35 452.236 110.25C452.236 102.35 453.786 95.3 456.886 89.1C460.086 82.9 464.386 78 469.786 74.4C475.286 70.8 481.536 69 488.536 69C492.236 69 495.786 69.6 499.186 70.8C502.686 71.9 505.786 73.45 508.486 75.45C511.286 77.45 513.536 79.7 515.236 82.2C516.936 84.6 517.886 87.15 518.086 89.85L512.686 90.75V39H533.986V150H513.886L512.986 131.7L517.186 132.15C516.986 134.65 516.086 137.05 514.486 139.35C512.886 141.65 510.736 143.75 508.036 145.65C505.436 147.45 502.436 148.9 499.036 150C495.736 151 492.236 151.5 488.536 151.5ZM493.336 133.8C497.336 133.8 500.836 132.8 503.836 130.8C506.836 128.8 509.186 126.05 510.886 122.55C512.586 119.05 513.436 114.95 513.436 110.25C513.436 105.65 512.586 101.6 510.886 98.1C509.186 94.5 506.836 91.75 503.836 89.85C500.836 87.85 497.336 86.85 493.336 86.85C489.336 86.85 485.836 87.85 482.836 89.85C479.936 91.75 477.636 94.5 475.936 98.1C474.336 101.6 473.536 105.65 473.536 110.25C473.536 114.95 474.336 119.05 475.936 122.55C477.636 126.05 479.936 128.8 482.836 130.8C485.836 132.8 489.336 133.8 493.336 133.8Z" />
</svg>

After

Width:  |  Height:  |  Size: 4.5 KiB

View File

@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 21.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 822.8 355.5" style="enable-background:new 0 0 822.8 355.5;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FFFFFF;}
.st1{fill:#FBAE40;}
.st2{fill:#F58220;}
</style>
<g id="Page-1">
<path id="CLOUDFLARE-_xAE_" class="st0" d="M772.2,252.6c-3.4,0-6.1-2.7-6.1-6.1c0-3.3,2.7-6.1,6.1-6.1c3.3,0,6.1,2.7,6.1,6.1
C778.3,249.8,775.5,252.6,772.2,252.6L772.2,252.6z M772.2,241.6c-2.7,0-4.9,2.2-4.9,4.9s2.2,4.9,4.9,4.9c2.7,0,4.9-2.2,4.9-4.9
S774.9,241.6,772.2,241.6L772.2,241.6z M775.3,249.7h-1.4l-1.2-2.3h-1.6v2.3h-1.3V243h3.2c1.4,0,2.3,0.9,2.3,2.2c0,1-0.6,1.7-1.4,2
L775.3,249.7z M772.9,246.2c0.5,0,1-0.3,1-1c0-0.8-0.4-1-1-1h-2v2H772.9z M136.7,239.8h15.6v42.5h27.1v13.6h-42.7V239.8z
M195.5,268v-0.2c0-16.1,13-29.2,30.3-29.2s30.1,12.9,30.1,29v0.2c0,16.1-13,29.2-30.3,29.2S195.5,284.1,195.5,268z M240.1,268
v-0.2c0-8.1-5.8-15.1-14.4-15.1c-8.5,0-14.2,6.9-14.2,15v0.2c0,8.1,5.8,15.1,14.3,15.1C234.4,283,240.1,276.1,240.1,268z
M275,271.3v-31.5h15.8V271c0,8.1,4.1,11.9,10.3,11.9c6.2,0,10.3-3.7,10.3-11.5v-31.6h15.8v31.1c0,18.1-10.3,26-26.3,26
C285,296.9,275,288.9,275,271.3z M351,239.8h21.6c20,0,31.7,11.5,31.7,27.7v0.2c0,16.2-11.8,28.2-32,28.2H351V239.8z M372.9,282.1
c9.3,0,15.5-5.1,15.5-14.2v-0.2c0-9-6.2-14.2-15.5-14.2h-6.3V282L372.9,282.1L372.9,282.1z M426.9,239.8h44.9v13.6h-29.4v9.6H469
v12.9h-26.6v20h-15.5V239.8z M493.4,239.8h15.5v42.5h27.2v13.6h-42.7V239.8z M576.7,239.4h15l23.9,56.5h-16.7l-4.1-10h-21.6l-4,10
h-16.3L576.7,239.4z M590.4,273.8l-6.2-15.9l-6.3,15.9H590.4z M635.6,239.8h26.5c8.6,0,14.5,2.2,18.3,6.1c3.3,3.2,5,7.5,5,13.1v0.2
c0,8.6-4.6,14.3-11.5,17.2l13.4,19.6h-18L658,279h-6.8v17h-15.6V239.8z M661.4,266.7c5.3,0,8.3-2.6,8.3-6.6v-0.2
c0-4.4-3.2-6.6-8.4-6.6h-10.2v13.4H661.4z M707.8,239.8h45.1V253h-29.7v8.5h26.9v12.3h-26.9v8.9h30.1v13.2h-45.5V239.8z
M102.7,274.6c-2.2,4.9-6.8,8.4-12.8,8.4c-8.5,0-14.3-7.1-14.3-15.1v-0.2c0-8.1,5.7-15,14.2-15c6.4,0,11.3,3.9,13.3,9.3h16.4
c-2.6-13.4-14.4-23.3-29.6-23.3c-17.3,0-30.3,13.1-30.3,29.2v0.2c0,16.1,12.8,29,30.1,29c14.8,0,26.4-9.6,29.4-22.4L102.7,274.6z"
/>
<path id="flare" class="st0" d="M734.5,150.4l-40.7-24.7c-0.6-0.1-4.4,0.3-6.4-0.7c-1.4-0.7-2.5-1.9-3.2-4c-3.2,0-175.5,0-175.5,0
v91.8h225.8V150.4z"/>
<path id="right-cloud" class="st1" d="M692.2,125.8c-0.8,0-1.5,0.6-1.8,1.4l-4.8,16.7c-2.1,7.2-1.3,13.8,2.2,18.7
c3.2,4.5,8.6,7.1,15.1,7.4l26.2,1.6c0.8,0,1.5,0.4,1.9,1c0.4,0.6,0.5,1.5,0.3,2.2c-0.4,1.2-1.6,2.1-2.9,2.2l-27.3,1.6
c-14.8,0.7-30.7,12.6-36.3,27.2l-2,5.1c-0.4,1,0.3,2,1.4,2H758c1.1,0,2.1-0.7,2.4-1.8c1.6-5.8,2.5-11.9,2.5-18.2
c0-37-30.2-67.2-67.3-67.2C694.5,125.7,693.3,125.7,692.2,125.8z"/>
<path id="left-cloud" class="st2" d="M656.4,204.6c2.1-7.2,1.3-13.8-2.2-18.7c-3.2-4.5-8.6-7.1-15.1-7.4L516,176.9
c-0.8,0-1.5-0.4-1.9-1c-0.4-0.6-0.5-1.4-0.3-2.2c0.4-1.2,1.6-2.1,2.9-2.2l124.2-1.6c14.7-0.7,30.7-12.6,36.3-27.2l7.1-18.5
c0.3-0.8,0.4-1.6,0.2-2.4c-8-36.2-40.3-63.2-78.9-63.2c-35.6,0-65.8,23-76.6,54.9c-7-5.2-15.9-8-25.5-7.1
c-17.1,1.7-30.8,15.4-32.5,32.5c-0.4,4.4-0.1,8.7,0.9,12.7c-27.9,0.8-50.2,23.6-50.2,51.7c0,2.5,0.2,5,0.5,7.5
c0.2,1.2,1.2,2.1,2.4,2.1h227.2c1.3,0,2.5-0.9,2.9-2.2L656.4,204.6z"/>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1,28 @@
@import "tailwindcss";
:root {
--background: #ffffff;
--foreground: #171717;
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
}
@theme {
--color-background: var(--background);
--color-foreground: var(--foreground);
}
body {
@apply bg-background text-foreground flex;
font-family: Arial, Helvetica, sans-serif;
}
#root {
width: 100%;
min-height: 100dvh;
}

View File

@@ -0,0 +1,13 @@
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import "./index.css";
import App from "./App.tsx";
import { ClientProvider } from "bknd/client";
createRoot(document.getElementById("root")!).render(
<StrictMode>
<ClientProvider>
<App />
</ClientProvider>
</StrictMode>
);

View File

@@ -0,0 +1,8 @@
import { Admin, type BkndAdminProps } from "bknd/ui";
import "bknd/dist/styles.css";
import { useAuth } from "bknd/client";
export default function AdminPage(props: BkndAdminProps) {
const auth = useAuth();
return <Admin {...props} withProvider={{ user: auth.user }} />;
}

View File

@@ -0,0 +1,94 @@
import { useAuth, useEntityQuery } from "bknd/client";
import bkndLogo from "../assets/bknd.svg";
import cloudflareLogo from "../assets/cloudflare.svg";
import viteLogo from "../assets/vite.svg";
export default function Home() {
const auth = useAuth();
const limit = 5;
const { data: todos, ...$q } = useEntityQuery("todos", undefined, {
limit,
sort: "-id",
});
return (
<div className="flex-col gap-10 max-w-96 mx-auto w-full min-h-full flex justify-center items-center">
<div className="flex flex-row items-center gap-3">
<img src={bkndLogo} alt="bknd" className="w-48 dark:invert" />
<div className="font-mono opacity-70">&amp;</div>
<div className="flex flex-row gap-2 items-center">
<img src={cloudflareLogo} alt="cloudflare" className="h-10" />
<div className="font-mono opacity-70">+</div>
<img src={viteLogo} alt="vite" className="w-10" />
</div>
</div>
<div className="flex flex-col border border-foreground/15 w-full py-4 px-5 gap-2">
<h2 className="font-mono mb-1 opacity-70">
<code>What's next?</code>
</h2>
<div className="flex flex-col w-full gap-2">
<div className="flex flex-col gap-3">
{todos &&
[...todos].reverse().map((todo) => (
<div className="flex flex-row" key={String(todo.id)}>
<div className="flex flex-row flex-grow items-center gap-3 ml-1">
<input
type="checkbox"
className="flex-shrink-0 cursor-pointer"
defaultChecked={!!todo.done}
onChange={async () => {
await $q.update({ done: !todo.done }, todo.id);
}}
/>
<div className="text-foreground/90 leading-none">{todo.title}</div>
</div>
<button
type="button"
className="cursor-pointer grayscale transition-all hover:grayscale-0 text-xs "
onClick={async () => {
await $q._delete(todo.id);
}}
>
</button>
</div>
))}
</div>
<form
className="flex flex-row w-full gap-3 mt-2"
key={todos?.map((t) => t.id).join()}
action={async (formData: FormData) => {
const title = formData.get("title") as string;
await $q.create({ title });
}}
>
<input
type="text"
name="title"
placeholder="New todo"
className="py-2 px-4 flex flex-grow rounded-sm bg-foreground/10 focus:bg-foreground/20 transition-colors outline-none"
/>
<button type="submit" className="cursor-pointer">
Add
</button>
</form>
</div>
</div>
<div className="flex flex-col items-center gap-1">
<a href="/admin">Go to Admin. </a>
<div className="opacity-50 text-sm">
{auth.user ? (
<p>
Authenticated as <b>{auth.user.email}</b>
</p>
) : (
<a href="/admin/auth/login">Login</a>
)}
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1 @@
/// <reference types="vite/client" />

View File

@@ -0,0 +1,7 @@
import { serve } from "bknd/adapter/cloudflare";
import config from "../../config.ts";
export default serve(config, () => ({
// since bknd is running code-only, we can use a pre-initialized app instance if available
warm: true,
}));

View File

@@ -0,0 +1,26 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["src/app", "./config.ts"]
}

View File

@@ -0,0 +1,11 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" },
{ "path": "./tsconfig.worker.json" }
],
"compilerOptions": {
"types": ["./worker-configuration.d.ts", "node"]
}
}

View File

@@ -0,0 +1,24 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2022",
"lib": ["ES2023"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["vite.config.ts"]
}

View File

@@ -0,0 +1,8 @@
{
"extends": "./tsconfig.node.json",
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.worker.tsbuildinfo",
"types": ["vite/client", "./worker-configuration.d.ts"]
},
"include": ["src/worker", "src/config.ts"]
}

View File

@@ -0,0 +1,14 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { cloudflare } from "@cloudflare/vite-plugin";
import tailwindcss from "@tailwindcss/vite";
export default defineConfig({
plugins: [react(), tailwindcss(), cloudflare()],
build: {
minify: true,
},
resolve: {
dedupe: ["react", "react-dom"],
},
});

View File

@@ -0,0 +1,47 @@
{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "cloudflare-vite-fullstack-bknd",
"main": "./src/worker/index.ts",
"compatibility_date": "2025-10-08",
"compatibility_flags": ["nodejs_compat"],
"observability": {
"enabled": true
},
"upload_source_maps": true,
"assets": {
"binding": "ASSETS",
"directory": "./dist/client",
"not_found_handling": "single-page-application",
"run_worker_first": ["/api*", "!/assets/*"]
},
"vars": {
"ENVIRONMENT": "development"
},
"d1_databases": [
{
"binding": "DB"
}
],
"r2_buckets": [
{
"binding": "BUCKET"
}
],
"env": {
"production": {
"vars": {
"ENVIRONMENT": "production"
},
"d1_databases": [
{
"binding": "DB"
}
],
"r2_buckets": [
{
"binding": "BUCKET"
}
]
}
}
}

View File

@@ -0,0 +1 @@
auth.jwt.secret=secret

View File

@@ -0,0 +1,249 @@
# bknd starter: Cloudflare Vite Hybrid
A fullstack React + Vite application with bknd integration, showcasing **hybrid mode** and Cloudflare Workers deployment.
## Key Features
This example demonstrates several advanced bknd features:
### 🔄 Hybrid Mode
Configure your backend **visually in development** using the Admin UI, then automatically switch to **code-only mode in production** for maximum performance. Changes made in the Admin UI are automatically synced to `bknd-config.json` and type definitions are generated in `bknd-types.d.ts`.
### 📁 Filesystem Access with Vite Plugin
Cloudflare's Vite plugin uses `unenv` which disables Node.js APIs like `fs`. This example uses bknd's `devFsVitePlugin` and `devFsWrite` to provide filesystem access during development, enabling automatic syncing of types and configuration.
### ⚡ Split Configuration Pattern
- **`config.ts`**: Shared configuration that can be safely imported in your worker
- **`bknd.config.ts`**: Wraps the configuration with `withPlatformProxy` for CLI usage with Cloudflare bindings (should NOT be imported in your worker)
This pattern prevents bundling `wrangler` into your worker while still allowing CLI access to Cloudflare resources.
## Project Structure
Inside of your project, you'll see the following folders and files:
```text
/
├── src/
│ ├── app/ # React frontend application
│ │ ├── App.tsx
│ │ ├── routes/
│ │ │ ├── admin.tsx # bknd Admin UI route
│ │ │ └── home.tsx # Example frontend route
│ │ └── main.tsx
│ └── worker/
│ └── index.ts # Cloudflare Worker entry
├── config.ts # Shared bknd configuration (hybrid mode)
├── bknd.config.ts # CLI configuration with platform proxy
├── bknd-config.json # Auto-generated production config
├── bknd-types.d.ts # Auto-generated TypeScript types
├── .env.example # Auto-generated secrets template
├── vite.config.ts # Includes devFsVitePlugin
├── package.json
└── wrangler.json # Cloudflare Workers configuration
```
## Cloudflare resources
- **D1:** `wrangler.json` declares a `DB` binding. In production, replace `database_id` with your own (`wrangler d1 create <name>`).
- **R2:** Optional `BUCKET` binding is pre-configured to show how to add additional services.
- **Environment awareness:** `ENVIRONMENT` switch toggles hybrid behavior: production makes the database read-only, while development keeps `mode: "db"` and auto-syncs schema.
- **Static assets:** The Assets binding points to `dist/client`. Run `npm run build` before `wrangler deploy` to upload the client bundle alongside the worker.
## Admin UI & frontend
- `/admin` mounts `<Admin />` from `bknd/ui` with `withProvider={{ user }}` so it respects the authenticated user returned by `useAuth`.
- `/` showcases `useEntityQuery("todos")`, mutation helpers, and authentication state — demonstrating how the generated client types (`bknd-types.d.ts`) flow into the React code.
## Configuration Files
### `config.ts`
The main configuration file that uses the `hybrid()` mode helper:
- Loads the generated config via an ESM `reader` (importing `./bknd-config.json`).
- Uses `devFsWrite` as the `writer` so the CLI/plugin can persist files even though Node's `fs` API is unavailable in Miniflare.
- Sets `typesFilePath`, `configFilePath`, and `syncSecrets` (writes `.env.example`) so config, types, and secret placeholders stay aligned.
- Seeds example data/users in `options.seed` when the database is empty.
- Disables the built-in admin controller because the React app renders `/admin` via `bknd/ui`.
```typescript
import { hybrid } from "bknd/modes";
import { devFsWrite, type CloudflareBkndConfig } from "bknd/adapter/cloudflare";
export default hybrid<CloudflareBkndConfig>({
// Special reader for Cloudflare Workers (no Node.js fs)
reader: async () => (await import("./bknd-config.json")).default,
// devFsWrite enables file writing via Vite plugin
writer: devFsWrite,
// Auto-sync these files in development
typesFilePath: "./bknd-types.d.ts",
configFilePath: "./bknd-config.json",
syncSecrets: {
enabled: true,
outFile: ".env.example",
format: "env",
},
app: (env) => ({
adminOptions: false, // Disabled - we render React app instead
isProduction: env.ENVIRONMENT === "production",
secrets: env,
// ... your configuration
}),
});
```
### `bknd.config.ts`
Wraps the configuration for CLI usage with Cloudflare bindings:
```typescript
import { withPlatformProxy } from "bknd/adapter/cloudflare/proxy";
import config from "./config.ts";
export default withPlatformProxy(config);
```
### `vite.config.ts`
Includes the `devFsVitePlugin` for filesystem access:
```typescript
import { devFsVitePlugin } from "bknd/adapter/cloudflare";
export default defineConfig({
plugins: [
// ...
devFsVitePlugin({ configFile: "config.ts" }),
cloudflare(),
],
});
```
## Commands
All commands are run from the root of the project, from a terminal:
| Command | Action |
|:-------------------|:----------------------------------------------------------|
| `npm install` | Installs dependencies and generates wrangler types |
| `npm run dev` | Starts local dev server with Vite at `localhost:5173` |
| `npm run build` | Builds the application for production |
| `npm run preview` | Builds and previews the production build locally |
| `npm run deploy` | Builds, syncs the schema and deploys to Cloudflare Workers|
| `npm run bknd` | Runs bknd CLI commands |
| `npm run bknd:types` | Generates TypeScript types from your schema |
| `npm run cf:types` | Generates Cloudflare Worker types from `wrangler.json` |
| `npm run check` | Type checks and does a dry-run deployment |
## Development Workflow
1. **Install dependencies:**
```sh
npm install
```
2. **Start development server:**
```sh
npm run dev
```
3. **Visit the Admin UI** at `http://localhost:5173/admin` to configure your backend visually:
- Create entities and fields
- Configure authentication
- Set up relationships
- Define permissions
4. **Watch for auto-generated files:**
- `bknd-config.json` - Production configuration
- `bknd-types.d.ts` - TypeScript types
- `.env.example` - Required secrets
5. **Use the CLI** for manual operations:
```sh
# Generate types manually
npm run bknd:types
# Sync the production database schema (only safe operations are applied)
CLOUDFLARE_ENV=production npm run bknd -- sync --force
```
## Before you deploy
If you're using a D1 database, make sure to create a database in your Cloudflare account and replace the `database_id` accordingly in `wrangler.json`:
```sh
npx wrangler d1 create my-database
```
Update `wrangler.json`:
```json
{
"d1_databases": [
{
"binding": "DB",
"database_name": "my-database",
"database_id": "your-database-id-here"
}
]
}
```
## Deployment
Deploy to Cloudflare Workers:
```sh
npm run deploy
```
This will:
1. Set `ENVIRONMENT=production` to activate code-only mode
2. Build the Vite application
3. Deploy to Cloudflare Workers using Wrangler
In production, bknd will:
- Use the configuration from `bknd-config.json` (read-only)
- Skip config validation for better performance
- Expect secrets to be provided via environment variables
## Environment Variables
Make sure to set your secrets in the Cloudflare Workers dashboard or via Wrangler:
```sh
# Example: Set JWT secret
npx wrangler secret put auth.jwt.secret
```
Check `.env.example` for all required secrets after running the app in development mode.
## How Hybrid Mode Works
```mermaid
graph LR
A[Development] -->|Visual Config| B[Admin UI]
B -->|Auto-sync| C[bknd-config.json]
B -->|Auto-sync| D[bknd-types.d.ts]
C -->|Deploy| E[Production]
E -->|Read-only| F[Code-only Mode]
```
1. **In Development:** `mode: "db"` - Configuration stored in database, editable via Admin UI
2. **Auto-sync:** Changes automatically written to `bknd-config.json` and types to `bknd-types.d.ts`
3. **In Production:** `mode: "code"` - Configuration read from `bknd-config.json`, no database overhead
## Why devFsVitePlugin?
Cloudflare's Vite plugin removes Node.js APIs for Workers compatibility. This breaks filesystem operations needed for:
- Auto-syncing TypeScript types (`syncTypes` plugin)
- Auto-syncing configuration (`syncConfig` plugin)
- Auto-syncing secrets (`syncSecrets` plugin)
The `devFsVitePlugin` + `devFsWrite` combination provides a workaround by using Vite's module system to enable file writes during development.
## Want to learn more?
- [Cloudflare Integration Documentation](https://docs.bknd.io/integration/cloudflare)
- [Hybrid Mode Guide](https://docs.bknd.io/usage/introduction#hybrid-mode)
- [Mode Helpers Documentation](https://docs.bknd.io/usage/introduction#mode-helpers)
- [Discord Community](https://discord.gg/952SFk8Tb8)

View File

@@ -0,0 +1,204 @@
{
"server": {
"cors": {
"origin": "*",
"allow_methods": [
"GET",
"POST",
"PATCH",
"PUT",
"DELETE"
],
"allow_headers": [
"Content-Type",
"Content-Length",
"Authorization",
"Accept"
],
"allow_credentials": true
},
"mcp": {
"enabled": false,
"path": "/api/system/mcp",
"logLevel": "emergency"
}
},
"data": {
"basepath": "/api/data",
"default_primary_format": "integer",
"entities": {
"todos": {
"type": "regular",
"fields": {
"id": {
"type": "primary",
"config": {
"format": "integer",
"fillable": false,
"required": false
}
},
"title": {
"type": "text",
"config": {
"required": false
}
},
"done": {
"type": "boolean",
"config": {
"required": false
}
}
},
"config": {
"sort_field": "id",
"sort_dir": "asc"
}
},
"users": {
"type": "system",
"fields": {
"id": {
"type": "primary",
"config": {
"format": "integer",
"fillable": false,
"required": false
}
},
"email": {
"type": "text",
"config": {
"required": true
}
},
"strategy": {
"type": "enum",
"config": {
"options": {
"type": "strings",
"values": [
"password"
]
},
"required": true,
"hidden": [
"update",
"form"
],
"fillable": [
"create"
]
}
},
"strategy_value": {
"type": "text",
"config": {
"fillable": [
"create"
],
"hidden": [
"read",
"table",
"update",
"form"
],
"required": true
}
},
"role": {
"type": "enum",
"config": {
"options": {
"type": "objects",
"values": []
},
"required": false
}
}
},
"config": {
"sort_field": "id",
"sort_dir": "asc"
}
}
},
"relations": {},
"indices": {
"idx_unique_users_email": {
"entity": "users",
"fields": [
"email"
],
"unique": true
},
"idx_users_strategy": {
"entity": "users",
"fields": [
"strategy"
],
"unique": false
},
"idx_users_strategy_value": {
"entity": "users",
"fields": [
"strategy_value"
],
"unique": false
}
}
},
"auth": {
"enabled": true,
"basepath": "/api/auth",
"entity_name": "users",
"allow_register": true,
"jwt": {
"secret": "",
"alg": "HS256",
"expires": 0,
"issuer": "bknd-cloudflare-example",
"fields": [
"id",
"email",
"role"
]
},
"cookie": {
"domain": "",
"path": "/",
"sameSite": "strict",
"secure": true,
"httpOnly": true,
"expires": 604800,
"partitioned": false,
"renew": true,
"pathSuccess": "/",
"pathLoggedOut": "/"
},
"strategies": {
"password": {
"enabled": true,
"type": "password",
"config": {
"hashing": "sha256"
}
}
},
"guard": {
"enabled": false
},
"roles": {}
},
"media": {
"enabled": false,
"basepath": "/api/media",
"entity_name": "media",
"storage": {}
},
"flows": {
"basepath": "/api/flows",
"flows": {}
}
}

View File

@@ -0,0 +1,22 @@
import type { DB } from "bknd";
import type { Insertable, Selectable, Updateable, Generated } from "kysely";
declare global {
type BkndEntity<T extends keyof DB> = Selectable<DB[T]>;
type BkndEntityCreate<T extends keyof DB> = Insertable<DB[T]>;
type BkndEntityUpdate<T extends keyof DB> = Updateable<DB[T]>;
}
export interface Todos {
id: Generated<number>;
title?: string;
done?: boolean;
}
interface Database {
todos: Todos;
}
declare module "bknd" {
interface DB extends Database {}
}

View File

@@ -0,0 +1,15 @@
/**
* This file gets automatically picked up by the bknd CLI. Since we're using cloudflare,
* we want to use cloudflare bindings (such as the database). To do this, we need to wrap
* the configuration with the `withPlatformProxy` helper function.
*
* Don't import this file directly in your app, otherwise "wrangler" will be bundled with your worker.
* That's why we split the configuration into two files: `bknd.config.ts` and `config.ts`.
*/
import { withPlatformProxy } from "bknd/adapter/cloudflare/proxy";
import config from "./config.ts";
export default withPlatformProxy(config, {
useProxy: true,
});

View File

@@ -0,0 +1,47 @@
/// <reference types="./worker-configuration.d.ts" />
import { devFsWrite, type CloudflareBkndConfig } from "bknd/adapter/cloudflare";
import { hybrid } from "bknd/modes";
export default hybrid<CloudflareBkndConfig>({
// normally you would use e.g. `readFile` from `node:fs/promises`, however, cloudflare using vite plugin removes all Node APIs, therefore we need to use the module system to import the config file
reader: async () => {
return (await import("./bknd-config.json").then((module) => module.default)) as any;
},
// a writer is required to sync the types and config. We're using a vite plugin that proxies writing files (since Node APIs are not available)
writer: devFsWrite,
// the generated types are loaded using our tsconfig, and is automatically available in all bknd APIs
typesFilePath: "./bknd-types.d.ts",
// on every change, this config file is updated. When it's time to deploy, this will be inlined into your worker
configFilePath: "./bknd-config.json",
// secrets will always be extracted from the configuration, we're writing an example env file to know which secrets we need to provide prior to deploying
syncSecrets: {
enabled: true,
outFile: ".env.example",
format: "env",
} as const,
app: (env) => ({
// we need to disable the admin controller using the vite plugin, since we want to render our own app
adminOptions: false,
// this is important to determine whether configuration should be read-only, or if the database should be automatically synced
isProduction: env.ENVIRONMENT === "production",
// we need to inject the secrets that gets merged into the configuration
secrets: env,
options: {
// the seed option is only executed if the database was empty
seed: async (ctx) => {
// create some entries
await ctx.em.mutator("todos").insertMany([
{ title: "Learn bknd", done: true },
{ title: "Build something cool", done: false },
]);
// and create a user
await ctx.app.module.auth.createUser({
email: "test@bknd.io",
password: "12345678",
});
},
},
}),
});

View File

@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>bknd + Vite + Cloudflare + React + TS</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/app/main.tsx"></script>
</body>
</html>

View File

@@ -0,0 +1,36 @@
{
"name": "cloudflare-vite-fullstack-bknd",
"description": "A template for building a React application with Vite, Cloudflare Workers, and bknd",
"private": true,
"type": "module",
"scripts": {
"build": "vite build",
"cf:types": "wrangler types",
"bknd": "node --experimental-strip-types node_modules/.bin/bknd",
"bknd:types": "bknd -- types",
"check": "tsc && vite build && wrangler deploy --dry-run",
"deploy": "CLOUDFLARE_ENV=production vite build && CLOUDFLARE_ENV=production npm run bknd -- sync --force && wrangler deploy",
"dev": "vite",
"preview": "npm run build && vite preview",
"postinstall": "npm run cf:types"
},
"dependencies": {
"@tailwindcss/vite": "^4.1.17",
"bknd": "file:../../app",
"hono": "4.10.6",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"tailwindcss": "^4.1.17",
"wouter": "^3.7.1"
},
"devDependencies": {
"@cloudflare/vite-plugin": "1.15.2",
"@types/node": "^24.10.1",
"@types/react": "19.2.6",
"@types/react-dom": "19.2.3",
"@vitejs/plugin-react": "5.1.1",
"typescript": "5.9.3",
"vite": "^7.2.4",
"wrangler": "^4.50.0"
}
}

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1,39 @@
import { Router, Switch, Route } from "wouter";
import Home from "./routes/home.tsx";
import { lazy, Suspense, useEffect, useState } from "react";
const Admin = lazy(() => import("./routes/admin.tsx"));
import { useAuth } from "bknd/client";
export default function App() {
const auth = useAuth();
const [verified, setVerified] = useState(false);
useEffect(() => {
auth.verify().then(() => setVerified(true));
}, []);
if (!verified) return null;
return (
<Router>
<Switch>
<Route path="/" component={Home} />
<Route path="/admin/*?">
<Suspense>
<Admin
config={{
basepath: "/admin",
logo_return_path: "/../",
}}
/>
</Suspense>
</Route>
<Route path="*">
<div className="w-full min-h-full flex justify-center items-center font-mono text-4xl">
404
</div>
</Route>
</Switch>
</Router>
);
}

View File

@@ -0,0 +1,14 @@
<svg
width="578"
height="188"
viewBox="0 0 578 188"
fill="black"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M41.5 34C37.0817 34 33.5 37.5817 33.5 42V146C33.5 150.418 37.0817 154 41.5 154H158.5C162.918 154 166.5 150.418 166.5 146V42C166.5 37.5817 162.918 34 158.5 34H41.5ZM123.434 113.942C124.126 111.752 124.5 109.42 124.5 107C124.5 94.2975 114.203 84 101.5 84C99.1907 84 96.9608 84.3403 94.8579 84.9736L87.2208 65.1172C90.9181 63.4922 93.5 59.7976 93.5 55.5C93.5 49.701 88.799 45 83 45C77.201 45 72.5 49.701 72.5 55.5C72.5 61.299 77.201 66 83 66C83.4453 66 83.8841 65.9723 84.3148 65.9185L92.0483 86.0256C87.1368 88.2423 83.1434 92.1335 80.7957 96.9714L65.4253 91.1648C65.4746 90.7835 65.5 90.3947 65.5 90C65.5 85.0294 61.4706 81 56.5 81C51.5294 81 47.5 85.0294 47.5 90C47.5 94.9706 51.5294 99 56.5 99C60.0181 99 63.0648 96.9814 64.5449 94.0392L79.6655 99.7514C78.9094 102.03 78.5 104.467 78.5 107C78.5 110.387 79.2321 113.603 80.5466 116.498L69.0273 123.731C67.1012 121.449 64.2199 120 61 120C55.201 120 50.5 124.701 50.5 130.5C50.5 136.299 55.201 141 61 141C66.799 141 71.5 136.299 71.5 130.5C71.5 128.997 71.1844 127.569 70.6158 126.276L81.9667 119.149C86.0275 125.664 93.2574 130 101.5 130C110.722 130 118.677 124.572 122.343 116.737L132.747 120.899C132.585 121.573 132.5 122.276 132.5 123C132.5 127.971 136.529 132 141.5 132C146.471 132 150.5 127.971 150.5 123C150.5 118.029 146.471 114 141.5 114C138.32 114 135.525 115.649 133.925 118.139L123.434 113.942Z"
/>
<path d="M243.9 151.5C240.4 151.5 237 151 233.7 150C230.4 149 227.4 147.65 224.7 145.95C222 144.15 219.75 142.15 217.95 139.95C216.15 137.65 215 135.3 214.5 132.9L219.3 131.1L218.25 149.7H198.15V39H219.45V89.25L215.4 87.6C216 85.2 217.15 82.9 218.85 80.7C220.55 78.4 222.7 76.4 225.3 74.7C227.9 72.9 230.75 71.5 233.85 70.5C236.95 69.5 240.15 69 243.45 69C250.35 69 256.5 70.8 261.9 74.4C267.3 77.9 271.55 82.75 274.65 88.95C277.85 95.15 279.45 102.25 279.45 110.25C279.45 118.25 277.9 125.35 274.8 131.55C271.7 137.75 267.45 142.65 262.05 146.25C256.75 149.75 250.7 151.5 243.9 151.5ZM238.8 133.35C242.8 133.35 246.25 132.4 249.15 130.5C252.15 128.5 254.5 125.8 256.2 122.4C257.9 118.9 258.75 114.85 258.75 110.25C258.75 105.75 257.9 101.75 256.2 98.25C254.6 94.75 252.3 92.05 249.3 90.15C246.3 88.25 242.8 87.3 238.8 87.3C234.8 87.3 231.3 88.25 228.3 90.15C225.3 92.05 222.95 94.75 221.25 98.25C219.55 101.75 218.7 105.75 218.7 110.25C218.7 114.85 219.55 118.9 221.25 122.4C222.95 125.8 225.3 128.5 228.3 130.5C231.3 132.4 234.8 133.35 238.8 133.35ZM308.312 126.15L302.012 108.6L339.512 70.65H367.562L308.312 126.15ZM288.062 150V39H309.362V150H288.062ZM341.762 150L313.262 114.15L328.262 102.15L367.412 150H341.762ZM371.675 150V70.65H392.075L392.675 86.85L388.475 88.65C389.575 85.05 391.525 81.8 394.325 78.9C397.225 75.9 400.675 73.5 404.675 71.7C408.675 69.9 412.875 69 417.275 69C423.275 69 428.275 70.2 432.275 72.6C436.375 75 439.425 78.65 441.425 83.55C443.525 88.35 444.575 94.3 444.575 101.4V150H423.275V103.05C423.275 99.45 422.775 96.45 421.775 94.05C420.775 91.65 419.225 89.9 417.125 88.8C415.125 87.6 412.625 87.1 409.625 87.3C407.225 87.3 404.975 87.7 402.875 88.5C400.875 89.2 399.125 90.25 397.625 91.65C396.225 93.05 395.075 94.65 394.175 96.45C393.375 98.25 392.975 100.2 392.975 102.3V150H382.475C380.175 150 378.125 150 376.325 150C374.525 150 372.975 150 371.675 150ZM488.536 151.5C481.636 151.5 475.436 149.75 469.936 146.25C464.436 142.65 460.086 137.8 456.886 131.7C453.786 125.5 452.236 118.35 452.236 110.25C452.236 102.35 453.786 95.3 456.886 89.1C460.086 82.9 464.386 78 469.786 74.4C475.286 70.8 481.536 69 488.536 69C492.236 69 495.786 69.6 499.186 70.8C502.686 71.9 505.786 73.45 508.486 75.45C511.286 77.45 513.536 79.7 515.236 82.2C516.936 84.6 517.886 87.15 518.086 89.85L512.686 90.75V39H533.986V150H513.886L512.986 131.7L517.186 132.15C516.986 134.65 516.086 137.05 514.486 139.35C512.886 141.65 510.736 143.75 508.036 145.65C505.436 147.45 502.436 148.9 499.036 150C495.736 151 492.236 151.5 488.536 151.5ZM493.336 133.8C497.336 133.8 500.836 132.8 503.836 130.8C506.836 128.8 509.186 126.05 510.886 122.55C512.586 119.05 513.436 114.95 513.436 110.25C513.436 105.65 512.586 101.6 510.886 98.1C509.186 94.5 506.836 91.75 503.836 89.85C500.836 87.85 497.336 86.85 493.336 86.85C489.336 86.85 485.836 87.85 482.836 89.85C479.936 91.75 477.636 94.5 475.936 98.1C474.336 101.6 473.536 105.65 473.536 110.25C473.536 114.95 474.336 119.05 475.936 122.55C477.636 126.05 479.936 128.8 482.836 130.8C485.836 132.8 489.336 133.8 493.336 133.8Z" />
</svg>

After

Width:  |  Height:  |  Size: 4.5 KiB

View File

@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 21.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 822.8 355.5" style="enable-background:new 0 0 822.8 355.5;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FFFFFF;}
.st1{fill:#FBAE40;}
.st2{fill:#F58220;}
</style>
<g id="Page-1">
<path id="CLOUDFLARE-_xAE_" class="st0" d="M772.2,252.6c-3.4,0-6.1-2.7-6.1-6.1c0-3.3,2.7-6.1,6.1-6.1c3.3,0,6.1,2.7,6.1,6.1
C778.3,249.8,775.5,252.6,772.2,252.6L772.2,252.6z M772.2,241.6c-2.7,0-4.9,2.2-4.9,4.9s2.2,4.9,4.9,4.9c2.7,0,4.9-2.2,4.9-4.9
S774.9,241.6,772.2,241.6L772.2,241.6z M775.3,249.7h-1.4l-1.2-2.3h-1.6v2.3h-1.3V243h3.2c1.4,0,2.3,0.9,2.3,2.2c0,1-0.6,1.7-1.4,2
L775.3,249.7z M772.9,246.2c0.5,0,1-0.3,1-1c0-0.8-0.4-1-1-1h-2v2H772.9z M136.7,239.8h15.6v42.5h27.1v13.6h-42.7V239.8z
M195.5,268v-0.2c0-16.1,13-29.2,30.3-29.2s30.1,12.9,30.1,29v0.2c0,16.1-13,29.2-30.3,29.2S195.5,284.1,195.5,268z M240.1,268
v-0.2c0-8.1-5.8-15.1-14.4-15.1c-8.5,0-14.2,6.9-14.2,15v0.2c0,8.1,5.8,15.1,14.3,15.1C234.4,283,240.1,276.1,240.1,268z
M275,271.3v-31.5h15.8V271c0,8.1,4.1,11.9,10.3,11.9c6.2,0,10.3-3.7,10.3-11.5v-31.6h15.8v31.1c0,18.1-10.3,26-26.3,26
C285,296.9,275,288.9,275,271.3z M351,239.8h21.6c20,0,31.7,11.5,31.7,27.7v0.2c0,16.2-11.8,28.2-32,28.2H351V239.8z M372.9,282.1
c9.3,0,15.5-5.1,15.5-14.2v-0.2c0-9-6.2-14.2-15.5-14.2h-6.3V282L372.9,282.1L372.9,282.1z M426.9,239.8h44.9v13.6h-29.4v9.6H469
v12.9h-26.6v20h-15.5V239.8z M493.4,239.8h15.5v42.5h27.2v13.6h-42.7V239.8z M576.7,239.4h15l23.9,56.5h-16.7l-4.1-10h-21.6l-4,10
h-16.3L576.7,239.4z M590.4,273.8l-6.2-15.9l-6.3,15.9H590.4z M635.6,239.8h26.5c8.6,0,14.5,2.2,18.3,6.1c3.3,3.2,5,7.5,5,13.1v0.2
c0,8.6-4.6,14.3-11.5,17.2l13.4,19.6h-18L658,279h-6.8v17h-15.6V239.8z M661.4,266.7c5.3,0,8.3-2.6,8.3-6.6v-0.2
c0-4.4-3.2-6.6-8.4-6.6h-10.2v13.4H661.4z M707.8,239.8h45.1V253h-29.7v8.5h26.9v12.3h-26.9v8.9h30.1v13.2h-45.5V239.8z
M102.7,274.6c-2.2,4.9-6.8,8.4-12.8,8.4c-8.5,0-14.3-7.1-14.3-15.1v-0.2c0-8.1,5.7-15,14.2-15c6.4,0,11.3,3.9,13.3,9.3h16.4
c-2.6-13.4-14.4-23.3-29.6-23.3c-17.3,0-30.3,13.1-30.3,29.2v0.2c0,16.1,12.8,29,30.1,29c14.8,0,26.4-9.6,29.4-22.4L102.7,274.6z"
/>
<path id="flare" class="st0" d="M734.5,150.4l-40.7-24.7c-0.6-0.1-4.4,0.3-6.4-0.7c-1.4-0.7-2.5-1.9-3.2-4c-3.2,0-175.5,0-175.5,0
v91.8h225.8V150.4z"/>
<path id="right-cloud" class="st1" d="M692.2,125.8c-0.8,0-1.5,0.6-1.8,1.4l-4.8,16.7c-2.1,7.2-1.3,13.8,2.2,18.7
c3.2,4.5,8.6,7.1,15.1,7.4l26.2,1.6c0.8,0,1.5,0.4,1.9,1c0.4,0.6,0.5,1.5,0.3,2.2c-0.4,1.2-1.6,2.1-2.9,2.2l-27.3,1.6
c-14.8,0.7-30.7,12.6-36.3,27.2l-2,5.1c-0.4,1,0.3,2,1.4,2H758c1.1,0,2.1-0.7,2.4-1.8c1.6-5.8,2.5-11.9,2.5-18.2
c0-37-30.2-67.2-67.3-67.2C694.5,125.7,693.3,125.7,692.2,125.8z"/>
<path id="left-cloud" class="st2" d="M656.4,204.6c2.1-7.2,1.3-13.8-2.2-18.7c-3.2-4.5-8.6-7.1-15.1-7.4L516,176.9
c-0.8,0-1.5-0.4-1.9-1c-0.4-0.6-0.5-1.4-0.3-2.2c0.4-1.2,1.6-2.1,2.9-2.2l124.2-1.6c14.7-0.7,30.7-12.6,36.3-27.2l7.1-18.5
c0.3-0.8,0.4-1.6,0.2-2.4c-8-36.2-40.3-63.2-78.9-63.2c-35.6,0-65.8,23-76.6,54.9c-7-5.2-15.9-8-25.5-7.1
c-17.1,1.7-30.8,15.4-32.5,32.5c-0.4,4.4-0.1,8.7,0.9,12.7c-27.9,0.8-50.2,23.6-50.2,51.7c0,2.5,0.2,5,0.5,7.5
c0.2,1.2,1.2,2.1,2.4,2.1h227.2c1.3,0,2.5-0.9,2.9-2.2L656.4,204.6z"/>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1,28 @@
@import "tailwindcss";
:root {
--background: #ffffff;
--foreground: #171717;
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
}
@theme {
--color-background: var(--background);
--color-foreground: var(--foreground);
}
body {
@apply bg-background text-foreground flex;
font-family: Arial, Helvetica, sans-serif;
}
#root {
width: 100%;
min-height: 100dvh;
}

View File

@@ -0,0 +1,13 @@
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import "./index.css";
import App from "./App.tsx";
import { ClientProvider } from "bknd/client";
createRoot(document.getElementById("root")!).render(
<StrictMode>
<ClientProvider>
<App />
</ClientProvider>
</StrictMode>
);

View File

@@ -0,0 +1,8 @@
import { Admin, type BkndAdminProps } from "bknd/ui";
import "bknd/dist/styles.css";
import { useAuth } from "bknd/client";
export default function AdminPage(props: BkndAdminProps) {
const auth = useAuth();
return <Admin {...props} withProvider={{ user: auth.user }} />;
}

View File

@@ -0,0 +1,99 @@
import { useAuth, useEntityQuery } from "bknd/client";
import bkndLogo from "../assets/bknd.svg";
import cloudflareLogo from "../assets/cloudflare.svg";
import viteLogo from "../assets/vite.svg";
export default function Home() {
const auth = useAuth();
const limit = 5;
const { data: todos, ...$q } = useEntityQuery("todos", undefined, {
limit,
sort: "-id",
});
return (
<div className="flex-col gap-10 max-w-96 mx-auto w-full min-h-full flex justify-center items-center">
<div className="flex flex-row items-center gap-3">
<img src={bkndLogo} alt="bknd" className="w-48 dark:invert" />
<div className="font-mono opacity-70">&amp;</div>
<div className="flex flex-row gap-2 items-center">
<img src={cloudflareLogo} alt="cloudflare" className="h-10" />
<div className="font-mono opacity-70">+</div>
<img src={viteLogo} alt="vite" className="w-10" />
</div>
</div>
<div className="flex flex-col border border-foreground/15 w-full py-4 px-5 gap-2">
<h2 className="font-mono mb-1 opacity-70">
<code>What's next?</code>
</h2>
<div className="flex flex-col w-full gap-2">
<div className="flex flex-col gap-3">
{todos &&
[...todos].reverse().map((todo) => (
<div className="flex flex-row" key={String(todo.id)}>
<div className="flex flex-row flex-grow items-center gap-3 ml-1">
<input
type="checkbox"
className="flex-shrink-0 cursor-pointer"
defaultChecked={!!todo.done}
onChange={async () => {
await $q.update(
{ done: !todo.done },
todo.id
);
}}
/>
<div className="text-foreground/90 leading-none">
{todo.title}
</div>
</div>
<button
type="button"
className="cursor-pointer grayscale transition-all hover:grayscale-0 text-xs "
onClick={async () => {
await $q._delete(todo.id);
}}
>
</button>
</div>
))}
</div>
<form
className="flex flex-row w-full gap-3 mt-2"
key={todos?.map((t) => t.id).join()}
action={async (formData: FormData) => {
const title = formData.get("title") as string;
await $q.create({ title });
}}
>
<input
type="text"
name="title"
placeholder="New todo"
className="py-2 px-4 flex flex-grow rounded-sm bg-foreground/10 focus:bg-foreground/20 transition-colors outline-none"
/>
<button type="submit" className="cursor-pointer">
Add
</button>
</form>
</div>
</div>
<div className="flex flex-col items-center gap-1">
<a href="/admin">Go to Admin. </a>
<div className="opacity-50 text-sm">
{auth.user ? (
<p>
Authenticated as <b>{auth.user.email}</b>
</p>
) : (
<a href="/admin/auth/login">Login</a>
)}
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1 @@
/// <reference types="vite/client" />

View File

@@ -0,0 +1,4 @@
import { serve } from "bknd/adapter/cloudflare";
import config from "../../config.ts";
export default serve(config);

View File

@@ -0,0 +1,26 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["src/app"]
}

View File

@@ -0,0 +1,11 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" },
{ "path": "./tsconfig.worker.json" }
],
"compilerOptions": {
"types": ["./worker-configuration.d.ts", "./bknd-types.d.ts", "node"]
}
}

View File

@@ -0,0 +1,24 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2022",
"lib": ["ES2023"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["vite.config.ts"]
}

View File

@@ -0,0 +1,8 @@
{
"extends": "./tsconfig.node.json",
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.worker.tsbuildinfo",
"types": ["vite/client", "./worker-configuration.d.ts"]
},
"include": ["src/worker", "src/config.ts"]
}

View File

@@ -0,0 +1,21 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { cloudflare } from "@cloudflare/vite-plugin";
import tailwindcss from "@tailwindcss/vite";
import { devFsVitePlugin } from "bknd/adapter/cloudflare";
export default defineConfig({
plugins: [
react(),
// this plugin provides filesystem access during development
devFsVitePlugin({ configFile: "config.ts" }) as any,
tailwindcss(),
cloudflare(),
],
build: {
minify: true,
},
resolve: {
dedupe: ["react", "react-dom"],
},
});

View File

@@ -0,0 +1,47 @@
{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "cloudflare-vite-fullstack-bknd",
"main": "./src/worker/index.ts",
"compatibility_date": "2025-10-08",
"compatibility_flags": ["nodejs_compat"],
"observability": {
"enabled": true
},
"upload_source_maps": true,
"assets": {
"binding": "ASSETS",
"directory": "./dist/client",
"not_found_handling": "single-page-application",
"run_worker_first": ["/api*", "!/assets/*"]
},
"vars": {
"ENVIRONMENT": "development"
},
"d1_databases": [
{
"binding": "DB"
}
],
"r2_buckets": [
{
"binding": "BUCKET"
}
],
"env": {
"production": {
"vars": {
"ENVIRONMENT": "production"
},
"d1_databases": [
{
"binding": "DB"
}
],
"r2_buckets": [
{
"binding": "BUCKET"
}
]
}
}
}