diff --git a/examples/.gitignore b/examples/.gitignore index d305846..6fec887 100644 --- a/examples/.gitignore +++ b/examples/.gitignore @@ -2,4 +2,5 @@ */bun.lock */deno.lock */node_modules -*/*.db \ No newline at end of file +*/*.db +*/worker-configuration.d.ts \ No newline at end of file diff --git a/examples/cloudflare-vite-code/.env.local b/examples/cloudflare-vite-code/.env.local new file mode 100644 index 0000000..ab1e1c5 --- /dev/null +++ b/examples/cloudflare-vite-code/.env.local @@ -0,0 +1 @@ +JWT_SECRET=secret \ No newline at end of file diff --git a/examples/cloudflare-vite-code/README.md b/examples/cloudflare-vite-code/README.md new file mode 100644 index 0000000..c54626f --- /dev/null +++ b/examples/cloudflare-vite-code/README.md @@ -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 `). +- **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 `` 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({ + 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) + diff --git a/examples/cloudflare-vite-code/bknd.config.ts b/examples/cloudflare-vite-code/bknd.config.ts new file mode 100644 index 0000000..8129c0e --- /dev/null +++ b/examples/cloudflare-vite-code/bknd.config.ts @@ -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, +}); diff --git a/examples/cloudflare-vite-code/config.ts b/examples/cloudflare-vite-code/config.ts new file mode 100644 index 0000000..a0f0c8c --- /dev/null +++ b/examples/cloudflare-vite-code/config.ts @@ -0,0 +1,43 @@ +/// + +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({ + 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`. + }), +}); diff --git a/examples/cloudflare-vite-code/index.html b/examples/cloudflare-vite-code/index.html new file mode 100644 index 0000000..5b7fd70 --- /dev/null +++ b/examples/cloudflare-vite-code/index.html @@ -0,0 +1,14 @@ + + + + + + + bknd + Vite + Cloudflare + React + TS + + + +
+ + + diff --git a/examples/cloudflare-vite-code/package.json b/examples/cloudflare-vite-code/package.json new file mode 100644 index 0000000..1c0a53c --- /dev/null +++ b/examples/cloudflare-vite-code/package.json @@ -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" + } +} diff --git a/examples/cloudflare-vite-code/public/vite.svg b/examples/cloudflare-vite-code/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/examples/cloudflare-vite-code/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/cloudflare-vite-code/seed.ts b/examples/cloudflare-vite-code/seed.ts new file mode 100644 index 0000000..e548e69 --- /dev/null +++ b/examples/cloudflare-vite-code/seed.ts @@ -0,0 +1,28 @@ +/// + +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); diff --git a/examples/cloudflare-vite-code/src/app/App.tsx b/examples/cloudflare-vite-code/src/app/App.tsx new file mode 100644 index 0000000..9aa6a5f --- /dev/null +++ b/examples/cloudflare-vite-code/src/app/App.tsx @@ -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 ( + + + + + + + + + +
+ 404 +
+
+
+
+ ); +} diff --git a/examples/cloudflare-vite-code/src/app/assets/bknd.svg b/examples/cloudflare-vite-code/src/app/assets/bknd.svg new file mode 100644 index 0000000..182ef92 --- /dev/null +++ b/examples/cloudflare-vite-code/src/app/assets/bknd.svg @@ -0,0 +1,14 @@ + + + + \ No newline at end of file diff --git a/examples/cloudflare-vite-code/src/app/assets/cloudflare.svg b/examples/cloudflare-vite-code/src/app/assets/cloudflare.svg new file mode 100644 index 0000000..3bb7ac0 --- /dev/null +++ b/examples/cloudflare-vite-code/src/app/assets/cloudflare.svg @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/examples/cloudflare-vite-code/src/app/assets/react.svg b/examples/cloudflare-vite-code/src/app/assets/react.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/examples/cloudflare-vite-code/src/app/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/cloudflare-vite-code/src/app/assets/vite.svg b/examples/cloudflare-vite-code/src/app/assets/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/examples/cloudflare-vite-code/src/app/assets/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/cloudflare-vite-code/src/app/index.css b/examples/cloudflare-vite-code/src/app/index.css new file mode 100644 index 0000000..4e0bdd8 --- /dev/null +++ b/examples/cloudflare-vite-code/src/app/index.css @@ -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; +} diff --git a/examples/cloudflare-vite-code/src/app/main.tsx b/examples/cloudflare-vite-code/src/app/main.tsx new file mode 100644 index 0000000..8d55eb2 --- /dev/null +++ b/examples/cloudflare-vite-code/src/app/main.tsx @@ -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( + + + + + +); diff --git a/examples/cloudflare-vite-code/src/app/routes/admin.tsx b/examples/cloudflare-vite-code/src/app/routes/admin.tsx new file mode 100644 index 0000000..93a4d9b --- /dev/null +++ b/examples/cloudflare-vite-code/src/app/routes/admin.tsx @@ -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 ; +} diff --git a/examples/cloudflare-vite-code/src/app/routes/home.tsx b/examples/cloudflare-vite-code/src/app/routes/home.tsx new file mode 100644 index 0000000..5d2197a --- /dev/null +++ b/examples/cloudflare-vite-code/src/app/routes/home.tsx @@ -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 ( +
+
+ bknd +
&
+
+ cloudflare +
+
+ vite +
+
+ +
+

+ What's next? +

+
+
+ {todos && + [...todos].reverse().map((todo) => ( +
+
+ { + await $q.update({ done: !todo.done }, todo.id); + }} + /> +
{todo.title}
+
+ +
+ ))} +
+
t.id).join()} + action={async (formData: FormData) => { + const title = formData.get("title") as string; + await $q.create({ title }); + }} + > + + +
+
+
+ +
+ Go to Admin. ➝ +
+ {auth.user ? ( +

+ Authenticated as {auth.user.email} +

+ ) : ( + Login + )} +
+
+
+ ); +} diff --git a/examples/cloudflare-vite-code/src/app/vite-env.d.ts b/examples/cloudflare-vite-code/src/app/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/examples/cloudflare-vite-code/src/app/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/examples/cloudflare-vite-code/src/worker/index.ts b/examples/cloudflare-vite-code/src/worker/index.ts new file mode 100644 index 0000000..7f22fcf --- /dev/null +++ b/examples/cloudflare-vite-code/src/worker/index.ts @@ -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, +})); diff --git a/examples/cloudflare-vite-code/tsconfig.app.json b/examples/cloudflare-vite-code/tsconfig.app.json new file mode 100644 index 0000000..643d6aa --- /dev/null +++ b/examples/cloudflare-vite-code/tsconfig.app.json @@ -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"] +} diff --git a/examples/cloudflare-vite-code/tsconfig.json b/examples/cloudflare-vite-code/tsconfig.json new file mode 100644 index 0000000..c7155af --- /dev/null +++ b/examples/cloudflare-vite-code/tsconfig.json @@ -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"] + } +} diff --git a/examples/cloudflare-vite-code/tsconfig.node.json b/examples/cloudflare-vite-code/tsconfig.node.json new file mode 100644 index 0000000..50130b3 --- /dev/null +++ b/examples/cloudflare-vite-code/tsconfig.node.json @@ -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"] +} diff --git a/examples/cloudflare-vite-code/tsconfig.worker.json b/examples/cloudflare-vite-code/tsconfig.worker.json new file mode 100644 index 0000000..8a7758a --- /dev/null +++ b/examples/cloudflare-vite-code/tsconfig.worker.json @@ -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"] +} diff --git a/examples/cloudflare-vite-code/vite.config.ts b/examples/cloudflare-vite-code/vite.config.ts new file mode 100644 index 0000000..7704b9f --- /dev/null +++ b/examples/cloudflare-vite-code/vite.config.ts @@ -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"], + }, +}); diff --git a/examples/cloudflare-vite-code/wrangler.json b/examples/cloudflare-vite-code/wrangler.json new file mode 100644 index 0000000..d47fedd --- /dev/null +++ b/examples/cloudflare-vite-code/wrangler.json @@ -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" + } + ] + } + } +} diff --git a/examples/cloudflare-vite-hybrid/.env.example b/examples/cloudflare-vite-hybrid/.env.example new file mode 100644 index 0000000..ab058b1 --- /dev/null +++ b/examples/cloudflare-vite-hybrid/.env.example @@ -0,0 +1 @@ +auth.jwt.secret=secret \ No newline at end of file diff --git a/examples/cloudflare-vite-hybrid/README.md b/examples/cloudflare-vite-hybrid/README.md new file mode 100644 index 0000000..5f19c07 --- /dev/null +++ b/examples/cloudflare-vite-hybrid/README.md @@ -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 `). +- **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 `` 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({ + // 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) + diff --git a/examples/cloudflare-vite-hybrid/bknd-config.json b/examples/cloudflare-vite-hybrid/bknd-config.json new file mode 100644 index 0000000..fb703aa --- /dev/null +++ b/examples/cloudflare-vite-hybrid/bknd-config.json @@ -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": {} + } +} \ No newline at end of file diff --git a/examples/cloudflare-vite-hybrid/bknd-types.d.ts b/examples/cloudflare-vite-hybrid/bknd-types.d.ts new file mode 100644 index 0000000..db7bae6 --- /dev/null +++ b/examples/cloudflare-vite-hybrid/bknd-types.d.ts @@ -0,0 +1,22 @@ +import type { DB } from "bknd"; +import type { Insertable, Selectable, Updateable, Generated } from "kysely"; + +declare global { + type BkndEntity = Selectable; + type BkndEntityCreate = Insertable; + type BkndEntityUpdate = Updateable; +} + +export interface Todos { + id: Generated; + title?: string; + done?: boolean; +} + +interface Database { + todos: Todos; +} + +declare module "bknd" { + interface DB extends Database {} +} \ No newline at end of file diff --git a/examples/cloudflare-vite-hybrid/bknd.config.ts b/examples/cloudflare-vite-hybrid/bknd.config.ts new file mode 100644 index 0000000..8129c0e --- /dev/null +++ b/examples/cloudflare-vite-hybrid/bknd.config.ts @@ -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, +}); diff --git a/examples/cloudflare-vite-hybrid/config.ts b/examples/cloudflare-vite-hybrid/config.ts new file mode 100644 index 0000000..2177649 --- /dev/null +++ b/examples/cloudflare-vite-hybrid/config.ts @@ -0,0 +1,47 @@ +/// + +import { devFsWrite, type CloudflareBkndConfig } from "bknd/adapter/cloudflare"; +import { hybrid } from "bknd/modes"; + +export default hybrid({ + // 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", + }); + }, + }, + }), +}); diff --git a/examples/cloudflare-vite-hybrid/index.html b/examples/cloudflare-vite-hybrid/index.html new file mode 100644 index 0000000..5b7fd70 --- /dev/null +++ b/examples/cloudflare-vite-hybrid/index.html @@ -0,0 +1,14 @@ + + + + + + + bknd + Vite + Cloudflare + React + TS + + + +
+ + + diff --git a/examples/cloudflare-vite-hybrid/package.json b/examples/cloudflare-vite-hybrid/package.json new file mode 100644 index 0000000..71e3ae9 --- /dev/null +++ b/examples/cloudflare-vite-hybrid/package.json @@ -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" + } +} diff --git a/examples/cloudflare-vite-hybrid/public/vite.svg b/examples/cloudflare-vite-hybrid/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/examples/cloudflare-vite-hybrid/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/cloudflare-vite-hybrid/src/app/App.tsx b/examples/cloudflare-vite-hybrid/src/app/App.tsx new file mode 100644 index 0000000..9aa6a5f --- /dev/null +++ b/examples/cloudflare-vite-hybrid/src/app/App.tsx @@ -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 ( + + + + + + + + + +
+ 404 +
+
+
+
+ ); +} diff --git a/examples/cloudflare-vite-hybrid/src/app/assets/bknd.svg b/examples/cloudflare-vite-hybrid/src/app/assets/bknd.svg new file mode 100644 index 0000000..182ef92 --- /dev/null +++ b/examples/cloudflare-vite-hybrid/src/app/assets/bknd.svg @@ -0,0 +1,14 @@ + + + + \ No newline at end of file diff --git a/examples/cloudflare-vite-hybrid/src/app/assets/cloudflare.svg b/examples/cloudflare-vite-hybrid/src/app/assets/cloudflare.svg new file mode 100644 index 0000000..3bb7ac0 --- /dev/null +++ b/examples/cloudflare-vite-hybrid/src/app/assets/cloudflare.svg @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/examples/cloudflare-vite-hybrid/src/app/assets/react.svg b/examples/cloudflare-vite-hybrid/src/app/assets/react.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/examples/cloudflare-vite-hybrid/src/app/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/cloudflare-vite-hybrid/src/app/assets/vite.svg b/examples/cloudflare-vite-hybrid/src/app/assets/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/examples/cloudflare-vite-hybrid/src/app/assets/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/cloudflare-vite-hybrid/src/app/index.css b/examples/cloudflare-vite-hybrid/src/app/index.css new file mode 100644 index 0000000..4e0bdd8 --- /dev/null +++ b/examples/cloudflare-vite-hybrid/src/app/index.css @@ -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; +} diff --git a/examples/cloudflare-vite-hybrid/src/app/main.tsx b/examples/cloudflare-vite-hybrid/src/app/main.tsx new file mode 100644 index 0000000..8d55eb2 --- /dev/null +++ b/examples/cloudflare-vite-hybrid/src/app/main.tsx @@ -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( + + + + + +); diff --git a/examples/cloudflare-vite-hybrid/src/app/routes/admin.tsx b/examples/cloudflare-vite-hybrid/src/app/routes/admin.tsx new file mode 100644 index 0000000..93a4d9b --- /dev/null +++ b/examples/cloudflare-vite-hybrid/src/app/routes/admin.tsx @@ -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 ; +} diff --git a/examples/cloudflare-vite-hybrid/src/app/routes/home.tsx b/examples/cloudflare-vite-hybrid/src/app/routes/home.tsx new file mode 100644 index 0000000..91cb1f2 --- /dev/null +++ b/examples/cloudflare-vite-hybrid/src/app/routes/home.tsx @@ -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 ( +
+
+ bknd +
&
+
+ cloudflare +
+
+ vite +
+
+ +
+

+ What's next? +

+
+
+ {todos && + [...todos].reverse().map((todo) => ( +
+
+ { + await $q.update( + { done: !todo.done }, + todo.id + ); + }} + /> +
+ {todo.title} +
+
+ +
+ ))} +
+
t.id).join()} + action={async (formData: FormData) => { + const title = formData.get("title") as string; + await $q.create({ title }); + }} + > + + +
+
+
+ +
+ Go to Admin. ➝ +
+ {auth.user ? ( +

+ Authenticated as {auth.user.email} +

+ ) : ( + Login + )} +
+
+
+ ); +} diff --git a/examples/cloudflare-vite-hybrid/src/app/vite-env.d.ts b/examples/cloudflare-vite-hybrid/src/app/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/examples/cloudflare-vite-hybrid/src/app/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/examples/cloudflare-vite-hybrid/src/worker/index.ts b/examples/cloudflare-vite-hybrid/src/worker/index.ts new file mode 100644 index 0000000..7bf59ff --- /dev/null +++ b/examples/cloudflare-vite-hybrid/src/worker/index.ts @@ -0,0 +1,4 @@ +import { serve } from "bknd/adapter/cloudflare"; +import config from "../../config.ts"; + +export default serve(config); diff --git a/examples/cloudflare-vite-hybrid/tsconfig.app.json b/examples/cloudflare-vite-hybrid/tsconfig.app.json new file mode 100644 index 0000000..23edca7 --- /dev/null +++ b/examples/cloudflare-vite-hybrid/tsconfig.app.json @@ -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"] +} diff --git a/examples/cloudflare-vite-hybrid/tsconfig.json b/examples/cloudflare-vite-hybrid/tsconfig.json new file mode 100644 index 0000000..b3e17e0 --- /dev/null +++ b/examples/cloudflare-vite-hybrid/tsconfig.json @@ -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"] + } +} diff --git a/examples/cloudflare-vite-hybrid/tsconfig.node.json b/examples/cloudflare-vite-hybrid/tsconfig.node.json new file mode 100644 index 0000000..50130b3 --- /dev/null +++ b/examples/cloudflare-vite-hybrid/tsconfig.node.json @@ -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"] +} diff --git a/examples/cloudflare-vite-hybrid/tsconfig.worker.json b/examples/cloudflare-vite-hybrid/tsconfig.worker.json new file mode 100644 index 0000000..8a7758a --- /dev/null +++ b/examples/cloudflare-vite-hybrid/tsconfig.worker.json @@ -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"] +} diff --git a/examples/cloudflare-vite-hybrid/vite.config.ts b/examples/cloudflare-vite-hybrid/vite.config.ts new file mode 100644 index 0000000..0a83dae --- /dev/null +++ b/examples/cloudflare-vite-hybrid/vite.config.ts @@ -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"], + }, +}); diff --git a/examples/cloudflare-vite-hybrid/wrangler.json b/examples/cloudflare-vite-hybrid/wrangler.json new file mode 100644 index 0000000..d47fedd --- /dev/null +++ b/examples/cloudflare-vite-hybrid/wrangler.json @@ -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" + } + ] + } + } +}