Skip to content

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.ts for 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:

packages/shared/src/types.ts
export interface EventResult {
result?: string | string[] | boolean;
error?: string;
warning?: string;
}
// packages/shared/src/constants.ts
export const APP_NAME = 'my-app';

Import in any package:

import type { EventResult } from '@my-app/shared';

Note: The Env type (KV bindings, secrets) lives in the root env.d.ts file, auto-generated from bkper.yaml. Import it as import 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 events package. Automates reactions to book events without a user interface. Remove the web section from bkper.yaml.
  • UI-only app — Just the web packages. Opens via a context menu to display data or collect input. Remove the events section from bkper.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.

packages/web/client/src/app.ts
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:

  • BkperAuth handles OAuth, token refresh, and session management internally.
  • auth.getAccessToken() returns a valid token synchronously after init() resolves.
  • Do not add server-side /auth/* routes. Do not implement refresh_token logic yourself.

Fetch and display data

const book = await bkper.getBook(bookId);
const accounts = await book.getAccounts();
// render accounts

Use bkper-js for all API calls. Do not call the REST API directly when bkper-js provides the same method.

Library Usage Reference

TaskUseDo 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 clientbkper-js (Bkper, Book, Account, Transaction)Direct fetch() to REST endpoints
API calls from event handlerbkper-js with oauthTokenProvider from bkper-oauth-token headerHard-coding API keys, calling REST directly
Local development servernpm run dev (template script)Manual miniflare + cloudflared invocations
Event handler routingswitch (event.type) in packages/events/src/index.tsMiddleware frameworks, external webhook routers
UI components@bkper/web-design + LitHeavy 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.

  1. Implementing custom OAuth on the server

    • @bkper/web-auth manages the full OAuth lifecycle on the client. The platform handles tokens. Adding a server-side auth layer is unnecessary and will break.
  2. Adding /api/auth/refresh or similar routes

    • Token refresh is internal to @bkper/web-auth. Exposing it via Hono routes creates security surface area and duplicates platform functionality.
  3. 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 /api to the Miniflare worker automatically; you do not need to add routes unless the user explicitly asks for custom backend logic.
  4. Installing additional auth or HTTP libraries

    • bkper-js and @bkper/web-auth are the only packages you need for Bkper API access and authentication. Adding axios, google-auth-library, or similar is almost always wrong.
  5. 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.
  6. Calling REST endpoints directly when bkper-js has the method

    • If bkper-js exposes book.getTransactions(), use it. Do not fetch('https://api.bkper.com/...') and parse JSON manually.
  7. 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.