App Architecture
Bkper platform apps use a single Worker with a client folder for the UI, /api/* routes with an OpenAPI contract, and /events handlers. Add shared code only when your app actually needs it.
Bkper platform apps use one Worker bundle per app and environment. The same Worker serves the browser client, app-defined /api/* routes, and Bkper event ingress at /events.
Treat /api/* as the reusable surface for app behavior. The bundled web client is one consumer of that API; scripts, external clients, and agents can call the same routes with bearer auth.
Structure
my-app/├── client/│ ├── index.html│ └── src/ — Frontend UI (Vite + Lit)├── server/│ └── src/│ ├── index.ts│ └── handlers/├── bkper.yaml├── package.json├── tsconfig.json└── vite.config.tsThe default template is intentionally not a monorepo. Add a shared package only when the app actually needs one.
Client
The client folder builds a browser UI with Lit and @bkper/web-design for consistent Bkper styling.
- Built with Vite — configured in the project’s
vite.config.tsfor fast builds and HMR during development - Built assets are deployed with the same Worker
- Communicates with Bkper via
bkper-js
This is where your app’s UI lives — book pickers, account lists, reports, forms.
Client authentication
The client authenticates users via the @bkper/web-auth SDK. OAuth is pre-configured on the platform — no client IDs, redirect URIs, or consent screens to set up.
import { Bkper } from 'bkper-js';import { BkperAuth } from '@bkper/web-auth';
const auth = new BkperAuth({ baseUrl: isLocalDev ? window.location.origin : undefined, onLoginSuccess: () => initializeApp(), onLoginRequired: () => showLoginButton(),});await auth.init();
const bkper = new Bkper({ oauthTokenProvider: async () => auth.getAccessToken(),});This is the canonical browser pattern. Do not implement custom OAuth flows, redirect handling, or token refresh — the SDK and platform handle everything. See the @bkper/web-auth API Reference for the full SDK documentation.
Server Worker
The server folder runs on Cloudflare Workers using Hono as the web framework. It handles:
- Serving the client’s static assets
- Custom API routes for your app’s backend logic under
/api/* - Bkper event ingress under
/events - Type-safe access to platform services (KV, secrets) via
c.env
import { Hono } from 'hono';import { Bkper, Book } from 'bkper-js';import type { Env } from '../../env.js';
const app = new Hono<{ Bindings: Env }>();
app.get('/api/v1/books', async c => { const bkper = new Bkper(); const books = await bkper.getBooks(); return c.json({ books });});
app.post('/events', async c => { const event: bkper.Event = await c.req.json(); const bkper = new Bkper(); const book = new Book(event.book, bkper.getConfig()); // route by event.type return c.json({ result: false });});
app.get('*', c => c.env.ASSETS.fetch(c.req.raw));
export default app;App API contract
Expose reusable app behavior through /api/* routes when it may be called by more than one client.
Use this shape:
- Routes — Thin Hono handlers under
/api/*. - Schemas — Typed request and response schemas for every route.
- Services — Business behavior in server-side service modules.
- OpenAPI — A machine-readable app API spec exposed at
/openapi.json.
The default template starts public routes under /api/v1/* and generates typed client code from the same OpenAPI contract used by the shipped web client. Keep that contract current so scripts, external clients, and agents can connect without reverse-engineering the UI.
App API endpoints use these URLs:
Production: https://{appId}.bkper.app/api/*Preview: https://{appId}-preview.bkper.app/api/*Local: http://localhost:8787/api/*The app OpenAPI spec lives at:
Production: https://{appId}.bkper.app/openapi.jsonPreview: https://{appId}-preview.bkper.app/openapi.jsonLocal: http://localhost:8787/openapi.jsonExample script call:
TOKEN="$(bkper auth token)"
curl \ -H "Authorization: Bearer ${TOKEN}" \ "https://my-app.bkper.app/api/v1/books"Replace my-app with the app id from bkper.yaml.
Server API authentication
For deployed apps, server API routes under /api/* require a standard bearer token on the incoming request:
const token = auth.getAccessToken();if (!token) throw new Error('Not authenticated');
const response = await fetch('/api/v1/data', { headers: { Authorization: `Bearer ${token}` },});Dispatch validates the bearer token before your Worker runs. It then strips the Authorization header and passes only an internal outbound context, so app code should not read user tokens from request headers.
When the server route calls Bkper, use bkper-js without a token provider:
import { Bkper } from 'bkper-js';
app.get('/api/v1/books', async c => { const bkper = new Bkper(); const books = await bkper.getBooks(); return c.json({ books: books.map(book => ({ id: book.getId(), name: book.getName(), })), });});Platform outbound auth injects the validated user’s OAuth token on exact Bkper API requests. Browser sessions only allow access to app web pages; they do not authorize /api/* server routes or create outbound auth context.
Event handler authentication
On the Bkper Platform, /events is an internal Bkper delivery channel on the same Worker. App code must not read bkper-oauth-token, bkper-agent-id, or Authorization headers.
Use the same server-side Bkper API pattern as /api/* routes:
const bkper = new Bkper();const book = new Book(event.book, bkper.getConfig());Dispatch consumes the Core-sent event access token and strips platform headers before invoking your Worker. Platform outbound auth injects the token and app agent identity when your event handler calls the Bkper API.
For self-hosted event handlers, you receive and process event auth headers directly because the platform outbound layer is not involved.
Event routing pattern
A typical server routes events by type and delegates to small handlers:
app.post('/events', async c => { const event: bkper.Event = await c.req.json();
if (!event.book) { return c.json({ error: 'Missing book in event payload' }, 400); }
const bkper = new Bkper(); const book = new Book(event.book, bkper.getConfig());
switch (event.type) { case 'TRANSACTION_CHECKED': return c.json(await handleTransactionChecked(book, event)); default: return c.json({ result: false }); }});Event handlers run at https://{appId}.bkper.app/events in production. During development, a Cloudflare tunnel routes events to the same local Worker.
See Event Handlers for patterns and details.
When you don’t need every part
The platform can host different shapes, but the default template starts as a full app because /api/* routes give the app a reusable contract as it grows.
Use these shapes intentionally:
- Full app — Client UI,
/api/*backend logic, and/eventsautomation in one Worker. This is the default growth path. - Event-only app — Keep
server/and omitdeployment.client. Automates reactions to book events without a user interface. - UI-only app — Use
client/and keep a minimal server Worker for static assets only when the behavior is truly local to the browser. Add/api/*as soon as the behavior should be reusable by scripts, external clients, or agents.
Simple App Patterns
These are the minimal, canonical patterns for common app tasks. Use them as starting points and resist adding complexity unless the user explicitly asks for it.
Client-only UI with authentication
The smallest useful app can keep browser-only display logic in client/. No custom server routes, no event handlers, no custom auth logic.
If the behavior should be reused by scripts, external clients, or agents, expose it through /api/* instead of keeping it only in the UI.
import { Bkper } from 'bkper-js';import { BkperAuth } from '@bkper/web-auth';
const auth = new BkperAuth({ baseUrl: window.location.origin.includes('localhost') ? undefined : window.location.origin, onLoginSuccess: () => render(), onLoginRequired: () => renderLogin(),});await auth.init();
const bkper = new Bkper({ oauthTokenProvider: async () => auth.getAccessToken(),});
async function render() { const books = await bkper.getBooks(); // render books}Key points:
BkperAuthhandles OAuth, token refresh, and session management internally.auth.getAccessToken()returns a valid token synchronously afterinit()resolves.- Do not add server-side
/auth/*routes. Do not implementrefresh_tokenlogic yourself.
Fetch and display data
const book = await bkper.getBook(bookId);const accounts = await book.getAccounts();// render accountsUse bkper-js for all API calls. Do not call the REST API directly when bkper-js provides the same method.
Library Usage Reference
| Task | Use | Do not use |
|---|---|---|
| Client authentication | @bkper/web-auth (BkperAuth, getAccessToken) | Custom OAuth flows, manual fetch('/auth/refresh'), google-auth-library in the browser |
| API calls from client | bkper-js (Bkper, Book, Account, Transaction) | Direct fetch() to REST endpoints |
API calls from app server /api/* route | Incoming Authorization: Bearer <token> + server-side new Bkper() | Reading OAuth tokens in server code, relying on browser sessions for API auth |
| API calls from platform event handler | Server-side new Bkper() in /events | Reading bkper-oauth-token or bkper-agent-id in platform app code |
| Local development server | npm run dev (template script) | Manual miniflare + cloudflared invocations |
| Event handler routing | switch (event.type) in server/src/index.ts or server/src/handlers/ | Middleware frameworks, external webhook routers |
| UI components | @bkper/web-design + Lit | Heavy UI frameworks unless the user explicitly requests them |
Common Pitfalls
Avoid these patterns even if they seem necessary. The platform or SDK already solves the problem.
-
Implementing custom OAuth on the server
@bkper/web-authmanages the full OAuth lifecycle on the client. The platform handles tokens. Adding a server-side auth layer is unnecessary and will break.
-
Adding
/api/auth/refreshor similar routes- Token refresh is internal to
@bkper/web-auth. Exposing it via Hono routes creates security surface area and duplicates platform functionality.
- Token refresh is internal to
-
Relying on browser sessions for server API auth
- Sessions let users open app web pages, but
/api/*routes requireAuthorization: Bearer <token>. Dispatch validates bearer tokens and platform outbound uses that validated context for Bkper API calls.
- Sessions let users open app web pages, but
-
Modifying
server/for a simple UI task- If the user only asked for a client-side feature, do not touch server routes. The Vite dev server proxies
/apito the Miniflare worker automatically. Add routes when the behavior should be reusable by the shipped client, scripts, external clients, or agents.
- If the user only asked for a client-side feature, do not touch server routes. The Vite dev server proxies
-
Installing additional auth or HTTP libraries
bkper-jsand@bkper/web-authare the only packages you need for Bkper API access and authentication. Addingaxios,google-auth-library, or similar is almost always wrong.
-
Creating event handlers when the user asked for a UI-only feature
- If the user says “show me a list of books in a popup,” that is a client-only task. Do not add
/eventslogic or subscribe to webhooks.
- If the user says “show me a list of books in a popup,” that is a client-only task. Do not add
-
Calling REST endpoints directly when
bkper-jshas the method- If
bkper-jsexposesbook.getTransactions(), use it. Do notfetch('https://api.bkper.com/...')and parse JSON manually.
- If
-
Reverse-engineering SDK internals
- Use the public API surface documented in the API reference. Do not read SDK source to find private methods or internal request patterns.