App Architecture
Bkper platform apps use a three-package monorepo: a Lit + Vite client for the UI, a Hono server on Cloudflare Workers for the API, and an events package for webhook-driven automation. Mix and match only what you need.
Bkper platform apps follow a three-package monorepo pattern. Each package handles a distinct concern, all deployed to the same {appId}.bkper.app domain.
Structure
packages/├── shared/ — Shared types and utilities├── web/│ ├── client/ — Frontend UI (Vite + Lit)│ └── server/ — Backend API (Hono)└── events/ — Event handler (webhooks)The packages are connected via Bun workspaces. Import shared code from @my-app/shared in any package.
Web client
The client package 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 - Static assets served by the web server handler
- Communicates with Bkper via
bkper-js
This is where your app’s UI lives — book pickers, account lists, reports, forms.
Web 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 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.
Web server
The server package 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
- Type-safe access to platform services (KV, secrets) via
c.env
import { Hono } from 'hono';import type { Env } from '../../../../env.js';
const app = new Hono<{ Bindings: Env }>();
app.get('/api/data', async c => { const cached = await c.env.KV.get('my-key'); return c.json({ data: cached });});
export default app;Events handler
The events package receives webhook calls from Bkper when subscribed events occur. It’s a separate Hono app that processes events and returns responses.
import { Hono } from 'hono';import { Bkper, Book } from 'bkper-js';import { handleTransactionChecked } from './handlers/transaction-checked.js';import type { Env } from '../../../env.js';
const app = new Hono<{ Bindings: Env }>().basePath('/events');
app.post('/', 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({ oauthTokenProvider: async () => c.req.header('bkper-oauth-token'), agentIdProvider: async () => c.req.header('bkper-agent-id'), }); 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 }); }});
export default app;Event handlers run at https://{appId}.bkper.app/events in production. During development, a Cloudflare tunnel routes events to your local machine.
See Event Handlers for patterns and details.
Shared package
Common types, utilities, and constants used across packages:
export interface EventResult { result?: string | string[] | boolean; error?: string; warning?: string;}
// packages/shared/src/constants.tsexport const APP_NAME = 'my-app';Import in any package:
import type { EventResult } from '@my-app/shared';Note: The
Envtype (KV bindings, secrets) lives in the rootenv.d.tsfile, auto-generated frombkper.yaml. Import it asimport type { Env } from '../../../env.js'— it is not part of the shared package.
When you don’t need all three
Not every app needs a UI, API, and event handler:
- Event-only app — Just the
eventspackage. Automates reactions to book events without a user interface. Remove thewebsection frombkper.yaml. - UI-only app — Just the
webpackages. Opens via a context menu to display data or collect input. Remove theeventssection frombkper.yaml. - Full app — All three packages. Interactive UI with backend logic and event-driven automation.
The template includes all three by default. Remove what you don’t need.
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 needs only the packages/web/client/ directory. No server routes, no event handlers, no custom auth logic.
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 event handler | bkper-js with oauthTokenProvider from bkper-oauth-token header | Hard-coding API keys, calling REST directly |
| Local development server | npm run dev (template script) | Manual miniflare + cloudflared invocations |
| Event handler routing | switch (event.type) in packages/events/src/index.ts | 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
-
Modifying
packages/web/server/for a simple UI task- If the user only asked for a client-side feature, do not touch the server package. The Vite dev server proxies
/apito the Miniflare worker automatically; you do not need to add routes unless the user explicitly asks for custom backend logic.
- If the user only asked for a client-side feature, do not touch the server package. 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 scaffold
packages/events/handlers 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 scaffold
-
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.