bknd is dead long live userbase
Run Tests / test (pull_request) Successful in 4m57s

This commit is contained in:
2026-09-27 19:34:06 +05:30
parent 05e0086fcd
commit 0bfcb5ad71
529 changed files with 2385 additions and 2924 deletions
+39 -39
View File
@@ -1,19 +1,19 @@
# bknd starter: Cloudflare Vite Hybrid
A fullstack React + Vite application with bknd integration, showcasing **hybrid mode** and Cloudflare Workers deployment.
# userbase starter: Cloudflare Vite Hybrid
A fullstack React + Vite application with userbase integration, showcasing **hybrid mode** and Cloudflare Workers deployment.
## Key Features
This example demonstrates several advanced bknd features:
This example demonstrates several advanced userbase 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`.
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 `userbase-config.json` and type definitions are generated in `userbase-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.
Cloudflare's Vite plugin uses `unenv` which disables Node.js APIs like `fs`. This example uses userbase'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)
- **`userbase.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.
@@ -27,15 +27,15 @@ Inside of your project, you'll see the following folders and files:
│ ├── app/ # React frontend application
│ │ ├── App.tsx
│ │ ├── routes/
│ │ │ ├── admin.tsx # bknd Admin UI route
│ │ │ ├── admin.tsx # userbase 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
├── config.ts # Shared userbase configuration (hybrid mode)
├── userbase.config.ts # CLI configuration with platform proxy
├── userbase-config.json # Auto-generated production config
├── userbase-types.d.ts # Auto-generated TypeScript types
├── .env.example # Auto-generated secrets template
├── vite.config.ts # Includes devFsVitePlugin
├── package.json
@@ -51,8 +51,8 @@ Inside of your project, you'll see the following folders and files:
## 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.
- `/admin` mounts `<Admin />` from `userbase/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 (`userbase-types.d.ts`) flow into the React code.
## Configuration Files
@@ -60,25 +60,25 @@ Inside of your project, you'll see the following folders and 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`).
- Loads the generated config via an ESM `reader` (importing `./userbase-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`.
- Disables the built-in admin controller because the React app renders `/admin` via `userbase/ui`.
```typescript
import { hybrid } from "bknd/modes";
import { devFsWrite, type CloudflareBkndConfig } from "bknd/adapter/cloudflare";
import { hybrid } from "userbase/modes";
import { devFsWrite, type CloudflareUserbaseConfig } from "userbase/adapter/cloudflare";
export default hybrid<CloudflareBkndConfig>({
export default hybrid<CloudflareUserbaseConfig>({
// Special reader for Cloudflare Workers (no Node.js fs)
reader: async () => (await import("./bknd-config.json")).default,
reader: async () => (await import("./userbase-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",
typesFilePath: "./userbase-types.d.ts",
configFilePath: "./userbase-config.json",
syncSecrets: {
enabled: true,
outFile: ".env.example",
@@ -93,11 +93,11 @@ export default hybrid<CloudflareBkndConfig>({
});
```
### `bknd.config.ts`
### `userbase.config.ts`
Wraps the configuration for CLI usage with Cloudflare bindings:
```typescript
import { withPlatformProxy } from "bknd/adapter/cloudflare/proxy";
import { withPlatformProxy } from "userbase/adapter/cloudflare/proxy";
import config from "./config.ts";
export default withPlatformProxy(config);
@@ -107,7 +107,7 @@ export default withPlatformProxy(config);
Includes the `devFsVitePlugin` for filesystem access:
```typescript
import { devFsVitePlugin } from "bknd/adapter/cloudflare";
import { devFsVitePlugin } from "userbase/adapter/cloudflare";
export default defineConfig({
plugins: [
@@ -129,8 +129,8 @@ All commands are run from the root of the project, from a terminal:
| `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 userbase` | Runs userbase CLI commands |
| `npm run userbase: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 |
@@ -153,17 +153,17 @@ All commands are run from the root of the project, from a terminal:
- Define permissions
4. **Watch for auto-generated files:**
- `bknd-config.json` - Production configuration
- `bknd-types.d.ts` - TypeScript types
- `userbase-config.json` - Production configuration
- `userbase-types.d.ts` - TypeScript types
- `.env.example` - Required secrets
5. **Use the CLI** for manual operations:
```sh
# Generate types manually
npm run bknd:types
npm run userbase:types
# Sync the production database schema (only safe operations are applied)
CLOUDFLARE_ENV=production npm run bknd -- sync --force
CLOUDFLARE_ENV=production npm run userbase -- sync --force
```
## Before you deploy
@@ -200,8 +200,8 @@ This will:
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)
In production, userbase will:
- Use the configuration from `userbase-config.json` (read-only)
- Skip config validation for better performance
- Expect secrets to be provided via environment variables
@@ -221,15 +221,15 @@ Check `.env.example` for all required secrets after running the app in developme
```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]
B -->|Auto-sync| C[userbase-config.json]
B -->|Auto-sync| D[userbase-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
2. **Auto-sync:** Changes automatically written to `userbase-config.json` and types to `userbase-types.d.ts`
3. **In Production:** `mode: "code"` - Configuration read from `userbase-config.json`, no database overhead
## Why devFsVitePlugin?
@@ -242,8 +242,8 @@ The `devFsVitePlugin` + `devFsWrite` combination provides a workaround by using
## 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)
- [Cloudflare Integration Documentation](https://docs.userbase.io/integration/cloudflare)
- [Hybrid Mode Guide](https://docs.userbase.io/usage/introduction#hybrid-mode)
- [Mode Helpers Documentation](https://docs.userbase.io/usage/introduction#mode-helpers)
- [Discord Community](https://discord.gg/952SFk8Tb8)
@@ -158,7 +158,7 @@
"secret": "",
"alg": "HS256",
"expires": 0,
"issuer": "bknd-cloudflare-example",
"issuer": "userbase-cloudflare-example",
"fields": [
"id",
"email",
+5 -5
View File
@@ -1,10 +1,10 @@
import type { DB } from "bknd";
import type { DB } from "userbase";
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]>;
type UserbaseEntity<T extends keyof DB> = Selectable<DB[T]>;
type UserbaseEntityCreate<T extends keyof DB> = Insertable<DB[T]>;
type UserbaseEntityUpdate<T extends keyof DB> = Updateable<DB[T]>;
}
export interface Todos {
@@ -17,6 +17,6 @@ interface Database {
todos: Todos;
}
declare module "bknd" {
declare module "userbase" {
interface DB extends Database {}
}
@@ -1,13 +1,13 @@
/**
* This file gets automatically picked up by the bknd CLI. Since we're using cloudflare,
* This file gets automatically picked up by the userbase 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`.
* That's why we split the configuration into two files: `userbase.config.ts` and `config.ts`.
*/
import { withPlatformProxy } from "bknd/adapter/cloudflare/proxy";
import { withPlatformProxy } from "userbase/adapter/cloudflare/proxy";
import config from "./config.ts";
export default withPlatformProxy(config, {
+9 -9
View File
@@ -1,19 +1,19 @@
/// <reference types="./worker-configuration.d.ts" />
import { devFsWrite, type CloudflareBkndConfig } from "bknd/adapter/cloudflare";
import { hybrid } from "bknd/modes";
import { devFsWrite, type CloudflareUserbaseConfig } from "userbase/adapter/cloudflare";
import { hybrid } from "userbase/modes";
export default hybrid<CloudflareBkndConfig>({
export default hybrid<CloudflareUserbaseConfig>({
// 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;
return (await import("./userbase-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",
// the generated types are loaded using our tsconfig, and is automatically available in all userbase APIs
typesFilePath: "./userbase-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",
configFilePath: "./userbase-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,
@@ -32,13 +32,13 @@ export default hybrid<CloudflareBkndConfig>({
seed: async (ctx) => {
// create some entries
await ctx.em.mutator("todos").insertMany([
{ title: "Learn bknd", done: true },
{ title: "Learn userbase", done: true },
{ title: "Build something cool", done: false },
]);
// and create a user
await ctx.app.module.auth.createUser({
email: "test@bknd.io",
email: "test@userbase.io",
password: "12345678",
});
},
+1 -1
View File
@@ -4,7 +4,7 @@
<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>
<title>userbase + Vite + Cloudflare + React + TS</title>
</head>
<body>
+6 -6
View File
@@ -1,22 +1,22 @@
{
"name": "cloudflare-vite-fullstack-bknd",
"description": "A template for building a React application with Vite, Cloudflare Workers, and bknd",
"name": "cloudflare-vite-fullstack-userbase",
"description": "A template for building a React application with Vite, Cloudflare Workers, and userbase",
"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",
"userbase": "node --experimental-strip-types node_modules/.bin/userbase",
"userbase:types": "userbase -- 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",
"deploy": "CLOUDFLARE_ENV=production vite build && CLOUDFLARE_ENV=production npm run userbase -- 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",
"userbase": "file:../../app",
"hono": "4.10.6",
"react": "^19.1.0",
"react-dom": "^19.1.0",
@@ -2,7 +2,7 @@ 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";
import { useAuth } from "userbase/client";
export default function App() {
const auth = useAuth();
@@ -2,7 +2,7 @@ import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import "./index.css";
import App from "./App.tsx";
import { ClientProvider } from "bknd/client";
import { ClientProvider } from "userbase/client";
createRoot(document.getElementById("root")!).render(
<StrictMode>
@@ -1,8 +1,8 @@
import { Admin, type BkndAdminProps } from "bknd/ui";
import "bknd/dist/styles.css";
import { useAuth } from "bknd/client";
import { Admin, type UserbaseAdminProps } from "userbase/ui";
import "userbase/dist/styles.css";
import { useAuth } from "userbase/client";
export default function AdminPage(props: BkndAdminProps) {
export default function AdminPage(props: UserbaseAdminProps) {
const auth = useAuth();
return <Admin {...props} withProvider={{ user: auth.user }} />;
}
@@ -1,5 +1,5 @@
import { useAuth, useEntityQuery } from "bknd/client";
import bkndLogo from "../assets/bknd.svg";
import { useAuth, useEntityQuery } from "userbase/client";
import userbaseLogo from "../assets/userbase.svg";
import cloudflareLogo from "../assets/cloudflare.svg";
import viteLogo from "../assets/vite.svg";
@@ -15,7 +15,7 @@ export default function Home() {
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" />
<img src={userbaseLogo} alt="userbase" 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" />
@@ -1,4 +1,4 @@
import { serve } from "bknd/adapter/cloudflare";
import { serve } from "userbase/adapter/cloudflare";
import config from "../../config.ts";
export default serve(config);
@@ -6,6 +6,6 @@
{ "path": "./tsconfig.worker.json" }
],
"compilerOptions": {
"types": ["./worker-configuration.d.ts", "./bknd-types.d.ts", "node"]
"types": ["./worker-configuration.d.ts", "./userbase-types.d.ts", "node"]
}
}
@@ -2,7 +2,7 @@ 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";
import { devFsVitePlugin } from "userbase/adapter/cloudflare";
export default defineConfig({
plugins: [
@@ -1,6 +1,6 @@
{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "cloudflare-vite-fullstack-bknd",
"name": "cloudflare-vite-fullstack-userbase",
"main": "./src/worker/index.ts",
"compatibility_date": "2025-10-08",
"compatibility_flags": ["nodejs_compat"],