Use when setting up Bknd SDK in a frontend application. Covers Api class initialization, token storage, auth state handling, React integration with BkndBrowserApp and useApp hook, framework-specific setup (Vite, Next.js, standalone), and TypeScript type registration.
npx claudepluginhub cameronapak/bknd-expert --plugin bknd-research-skillsThis skill uses the workspace's default tool permissions.
Set up the Bknd TypeScript SDK in your frontend application.
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Searches prompts.chat for AI prompt templates by keyword or category, retrieves by ID with variable handling, and improves prompts via AI. Use for discovering or enhancing prompts.
Checks Next.js compilation errors using a running Turbopack dev server after code edits. Fixes actionable issues before reporting complete. Replaces `next build`.
Set up the Bknd TypeScript SDK in your frontend application.
bknd package installed: npm install bkndNot applicable - client setup is code-only.
Use when connecting to a separate Bknd backend server.
import { Api } from "bknd";
const api = new Api({
host: "https://api.example.com", // Your Bknd backend URL
});
// Make requests
const { ok, data } = await api.data.readMany("posts");
Store auth tokens across page refreshes:
import { Api } from "bknd";
const api = new Api({
host: "https://api.example.com",
storage: localStorage, // Persists token as "auth" key
});
Custom storage key:
const api = new Api({
host: "https://api.example.com",
storage: localStorage,
key: "myapp_auth", // Custom key instead of "auth"
});
React to login/logout events:
const api = new Api({
host: "https://api.example.com",
storage: localStorage,
onAuthStateChange: (state) => {
console.log("Auth state:", state);
// state.user - current user or undefined
// state.token - JWT or undefined
// state.verified - whether token was verified with server
},
});
For server-side rendering with cookies:
const api = new Api({
host: "https://api.example.com",
credentials: "include", // Send cookies cross-origin
});
import { Api } from "bknd";
const api = new Api({
// Required
host: "https://api.example.com",
// Auth persistence
storage: localStorage,
key: "auth",
// Auth events
onAuthStateChange: (state) => {
if (state.user) {
console.log("Logged in:", state.user.email);
} else {
console.log("Logged out");
}
},
// Request options
credentials: "include", // For cookies
verbose: true, // Log requests (dev only)
// Data API defaults
data: {
defaultQuery: { limit: 20 },
},
});
export { api };
Use when Bknd runs entirely in the browser (Vite + React).
// bknd.config.ts
import { boolean, em, entity, text } from "bknd";
export const schema = em({
todos: entity("todos", {
title: text(),
done: boolean(),
}),
});
// Type registration for autocomplete
type Database = (typeof schema)["DB"];
declare module "bknd" {
interface DB extends Database {}
}
// App.tsx
import { BkndBrowserApp, type BrowserBkndConfig } from "bknd/adapter/browser";
import { schema } from "./bknd.config";
const config = {
config: {
data: schema.toJSON(),
auth: {
enabled: true,
jwt: {
secret: "your-secret-key", // Use env var in production
},
},
},
options: {
seed: async (ctx) => {
// Initial data (runs once on empty DB)
await ctx.em.mutator("todos").insertMany([
{ title: "Learn bknd", done: false },
]);
},
},
} satisfies BrowserBkndConfig;
export default function App() {
return (
<BkndBrowserApp {...config}>
<YourRoutes />
</BkndBrowserApp>
);
}
import { useApp } from "bknd/adapter/browser";
function TodoList() {
const { api, app, user, isLoading } = useApp();
if (isLoading) return <div>Loading...</div>;
// api - Api instance for data/auth/media
// app - Full App instance
// user - Current user or null
// isLoading - True while initializing
const [todos, setTodos] = useState([]);
useEffect(() => {
api.data.readMany("todos").then(({ data }) => setTodos(data));
}, [api]);
return (
<ul>
{todos.map((todo) => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
);
}
Use when React connects to a separate Bknd server.
// lib/api.ts
import { Api } from "bknd";
export const api = new Api({
host: import.meta.env.VITE_BKND_URL || "http://localhost:7654",
storage: localStorage,
});
// context/BkndContext.tsx
import { createContext, useContext, useEffect, useState, ReactNode } from "react";
import { Api, type TApiUser } from "bknd";
import { api } from "../lib/api";
type BkndContextType = {
api: Api;
user: TApiUser | null;
isLoading: boolean;
};
const BkndContext = createContext<BkndContextType | null>(null);
export function BkndProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<TApiUser | null>(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
// Listen for auth changes
api.options.onAuthStateChange = (state) => {
setUser(state.user ?? null);
};
// Verify existing token on mount
api.verifyAuth().finally(() => {
setUser(api.getUser());
setIsLoading(false);
});
return () => {
api.options.onAuthStateChange = undefined;
};
}, []);
return (
<BkndContext.Provider value={{ api, user, isLoading }}>
{children}
</BkndContext.Provider>
);
}
export function useBknd() {
const ctx = useContext(BkndContext);
if (!ctx) throw new Error("useBknd must be used within BkndProvider");
return ctx;
}
// main.tsx
import { BkndProvider } from "./context/BkndContext";
ReactDOM.createRoot(document.getElementById("root")!).render(
<BkndProvider>
<App />
</BkndProvider>
);
function Profile() {
const { api, user, isLoading } = useBknd();
if (isLoading) return <div>Loading...</div>;
if (!user) return <div>Not logged in</div>;
return <div>Hello, {user.email}</div>;
}
// lib/bknd.ts
import { Api } from "bknd";
// Client-side singleton
let clientApi: Api | null = null;
export function getClientApi() {
if (typeof window === "undefined") {
throw new Error("getClientApi can only be used client-side");
}
if (!clientApi) {
clientApi = new Api({
host: process.env.NEXT_PUBLIC_BKND_URL!,
storage: localStorage,
});
}
return clientApi;
}
// Server-side (per-request)
export function getServerApi(request?: Request) {
return new Api({
host: process.env.BKND_URL!,
request, // Extracts token from cookies/headers
});
}
"use client";
import { useEffect, useState } from "react";
import { getClientApi } from "@/lib/bknd";
export function PostList() {
const [posts, setPosts] = useState([]);
const api = getClientApi();
useEffect(() => {
api.data.readMany("posts").then(({ data }) => setPosts(data));
}, []);
return <ul>{posts.map((p) => <li key={p.id}>{p.title}</li>)}</ul>;
}
// app/posts/page.tsx
import { getServerApi } from "@/lib/bknd";
export default async function PostsPage() {
const api = getServerApi();
const { data: posts } = await api.data.readMany("posts");
return <ul>{posts.map((p) => <li key={p.id}>{p.title}</li>)}</ul>;
}
Get autocomplete for entity names and fields:
// types/bknd.d.ts
import { em, entity, text, number } from "bknd";
// Define your schema
const schema = em({
posts: entity("posts", {
title: text(),
views: number(),
}),
users: entity("users", {
email: text(),
name: text(),
}),
});
// Register types globally
type Database = (typeof schema)["DB"];
declare module "bknd" {
interface DB extends Database {}
}
Now api.data.readMany("posts") returns typed Post[].
// Auth state
api.getUser() // Current user or null
api.isAuthenticated() // Has valid token
api.isAuthVerified() // Token verified with server
api.verifyAuth() // Verify token (async)
api.getAuthState() // { token, user, verified }
// Module APIs
api.data.readMany(...) // CRUD operations
api.auth.login(...) // Authentication
api.media.upload(...) // File uploads
api.system.health() // System checks
// Token management
api.updateToken(token) // Manually set token
api.token_transport // "header" | "cookie" | "none"
# .env.local (Next.js)
NEXT_PUBLIC_BKND_URL=http://localhost:7654
BKND_URL=http://localhost:7654
# .env (Vite)
VITE_BKND_URL=http://localhost:7654
Problem: User logged out after refresh
Fix: Provide storage option:
// WRONG - no persistence
const api = new Api({ host: "..." });
// CORRECT
const api = new Api({
host: "...",
storage: localStorage,
});
Problem: Browser blocks requests to backend
Fix: Configure CORS on backend:
// bknd.config.ts (server)
const app = new App({
server: {
cors: {
origin: ["http://localhost:3000"],
credentials: true,
},
},
});
Problem: UI doesn't reflect login/logout
Fix: Use onAuthStateChange:
const api = new Api({
host: "...",
onAuthStateChange: (state) => {
// Update your UI state here
setUser(state.user ?? null);
},
});
Problem: Server/client render different content
Fix: Check auth on client only:
function AuthStatus() {
const [user, setUser] = useState(null);
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
setUser(api.getUser());
}, []);
if (!mounted) return null; // Avoid hydration mismatch
return user ? <span>{user.email}</span> : <span>Guest</span>;
}
Problem: Import errors
Fix: Use correct subpath:
// Standalone API
import { Api } from "bknd";
// React browser adapter
import { BkndBrowserApp, useApp } from "bknd/adapter/browser";
// Next.js adapter
import type { NextjsBkndConfig } from "bknd/adapter/nextjs";
Problem: Auth state inconsistent across app
Fix: Use singleton pattern:
// lib/api.ts
let api: Api | null = null;
export function getApi() {
if (!api) {
api = new Api({ host: "...", storage: localStorage });
}
return api;
}
Test your setup:
import { api } from "./lib/api";
async function test() {
// 1. Check connection
const { ok } = await api.data.readMany("posts", { limit: 1 });
console.log("API connected:", ok);
// 2. Check auth
console.log("Authenticated:", api.isAuthenticated());
console.log("User:", api.getUser());
// 3. Test login
const { ok: loginOk } = await api.auth.login("password", {
email: "test@example.com",
password: "password123",
});
console.log("Login:", loginOk);
}
DO:
storage: localStorage for token persistenceonAuthStateChange for reactive UIverifyAuth() on app startupDON'T: